/// <summary> /// Generates table function mapping. /// </summary> /// <param name="tableFunction">Function model.</param> /// <param name="functionsGroup">Functions region.</param> /// <param name="context">Data context class.</param> private void BuildTableFunction( TableFunctionModel tableFunction, RegionGroup functionsGroup, CodeClass context) { // generated code sample: /* * #region Function1 * private static readonly MethodInfo _function1 = MemberHelper.MethodOf<DataContext>(ctx => ctx.Function1(default)); * * [Sql.TableFunction("Function1")] * public IQueryable<Parent> Function1(int? id) * { * return this.GetTable<Parent>(this, _function1, id); * } * #endregion */ // create function region var region = functionsGroup.New(tableFunction.Method.Name); // if function schema load failed, generate error pragma with exception details if (tableFunction.Error != null) { if (_options.DataModel.GenerateProceduresSchemaError) { region.Pragmas().Add(AST.Error($"Failed to load return table schema: {tableFunction.Error}")); } // as we cannot generate table function without knowing it's schema, we skip failed function return; } // if function result schema matches known entity, we use entity class for result // otherwise we generate custom record mapping if (tableFunction.Result == null || tableFunction.Result.CustomTable == null && tableFunction.Result.Entity == null) { throw new InvalidOperationException($"Table function {tableFunction.Name} result record type not set"); } // GetTable API for table functions need MethodInfo instance of generated method as parameter // to not load it on each call, we cache MethodInfo instance in static field var methodInfo = region .Fields(false) .New(AST.Name(tableFunction.MethodInfoFieldName), WellKnownTypes.System.Reflection.MethodInfo) .Private() .Static() .ReadOnly(); // generate mapping method with metadata var method = DefineMethod(region.Methods(false), tableFunction.Method); _metadataBuilder.BuildTableFunctionMetadata(tableFunction.Metadata, method); // generate method parameters, return type and body // table record type IType returnEntity; if (tableFunction.Result.Entity != null) { returnEntity = _entityBuilders[tableFunction.Result.Entity].Type.Type; } else { returnEntity = BuildCustomResultClass(tableFunction.Result.CustomTable !, region.Classes(), true).resultClassType; } // set return type // T4 used ITable<T> for return type, but there is no reason to use ITable<T> over IQueryable<T> // Even more: ITable<T> is not correct return type here var returnType = _options.DataModel.TableFunctionReturnsTable ? WellKnownTypes.LinqToDB.ITable(returnEntity) : WellKnownTypes.System.Linq.IQueryable(returnEntity); method.Returns(returnType); // parameters for GetTable call in mapping body var parameters = new ICodeExpression[3 + tableFunction.Parameters.Count]; parameters[0] = context.This; // `this` extension method parameter parameters[1] = context.This; // context parameter parameters[2] = methodInfo.Field.Reference; // method info field // add table function parameters (if any) var fieldInitParameters = new ICodeExpression[tableFunction.Parameters.Count]; for (var i = 0; i < tableFunction.Parameters.Count; i++) { var param = tableFunction.Parameters[i]; // parameters added to 3 places: // - to mapping method // - to GetTable call in mapping // - to mapping call in MethodInfo initializer we add parameter's default value var parameter = DefineParameter(method, param.Parameter); parameters[i + 3] = parameter.Reference; // TODO: potential issue: target-typed `default` could cause errors with overloads fieldInitParameters[i] = AST.Default(param.Parameter.Type, true); } // generate mapping body method.Body() .Append( AST.Return( AST.ExtCall( WellKnownTypes.LinqToDB.DataExtensions, WellKnownTypes.LinqToDB.DataExtensions_GetTable, WellKnownTypes.LinqToDB.ITable(returnEntity), new[] { returnEntity }, false, parameters))); // generate MethodInfo field initializer var lambdaParam = AST.LambdaParameter(AST.Name(TABLE_FUNCTION_METHOD_INFO_CONTEXT_PARAMETER), context.Type); // Expression<Func<context, returnType>> var lambda = AST .Lambda(WellKnownTypes.System.Linq.Expressions.Expression(WellKnownTypes.System.Func(returnType, context.Type)), true) .Parameter(lambdaParam); lambda.Body() .Append( AST.Return( AST.Call( lambdaParam.Reference, method.Method.Name, returnType, fieldInitParameters))); methodInfo.AddInitializer( AST.Call( new CodeTypeReference(WellKnownTypes.LinqToDB.Expressions.MemberHelper), WellKnownTypes.LinqToDB.Expressions.MemberHelper_MethodOf, WellKnownTypes.System.Reflection.MethodInfo, new[] { functionsGroup.OwnerType.Type }, false, lambda.Method)); // TODO: similar tables }
// IMPORTANT: // ExecuteProc/QueryProc APIs currently support only DataConnection context, so if // context is not based on DataConnection, we should generate DataConnection parameter // for context isntead of typed generated context // // TODO: needs linq2db refactoring // Note that we shouldn't fix it by extending current API to be available to DataContext // as current API needs refactoring to work with FQN components of procedure name // It will be more productive to invest time into new API implementation instead with FQN support on all // context types /// <summary> /// Generates stored procedure mapping. /// </summary> /// <param name="storedProcedure">Stored procedure model.</param> /// <param name="proceduresGroup">Stored procedures region.</param> /// <param name="dataContextType">Data context class type.</param> private void BuildStoredProcedure( StoredProcedureModel storedProcedure, Func <RegionGroup> proceduresGroup, IType dataContextType) { // TODO: refactor procedures generation logic. it became chaotic after async support added // generated code sample (without async version): /* * #region Procedure1 * public static IEnumerable<Procedure1Result> Procedure1(this DataConnection dataConnection, int? input, ref int? output) * { * var parameters = new [] * { * new DataParameter("@input", input, DataType.Int32), * new DataParameter("@output", output, DataType.Int32) * { * Direction = ParameterDirection.InputOutput * } * }; * var ret = dataConnection.QueryProc<Procedure1Result>("Procedure1", parameters).ToList(); * output = Converter.ChangeTypeTo<int?>(parameters[2].Value); * return ret; * } * * public partial class Procedure1Result * { * [LinqToDB.Mapping.Column("Column", CanBeNull = false, DbType = "nvarchar(8)", DataType = DataType.NVarChar, SkipOnInsert = true, SkipOnUpdate = true)] public string Column { get; set; } = null!; * } * #endregion */ // some notes: // - procedure could return no data sets or return records matching known entity, so *Result class generation is optional // - for result set with nameless/duplicate columns we generate ordinal reader instead of by-column-name mapping // stored procedure region that will contain procedure method mapping and optionally // result record mapping RegionBuilder?region = null; if (storedProcedure.Error != null) { // if procedure resultset schema load failed, generate error pragma if (_options.DataModel.GenerateProceduresSchemaError) { (region ??= proceduresGroup().New(storedProcedure.Method.Name)) .Pragmas() .Add(AST.Error(storedProcedure.Error)); } // even with this error procedure generation could continue, as we still // can invoke procedure, we just cannot get resultset from it if (_options.DataModel.SkipProceduresWithSchemaErrors) { return; } } var methodsGroup = (region ??= proceduresGroup().New(storedProcedure.Method.Name)).Methods(false); var useOrdinalMapping = false; ResultTableModel?customTable = null; IType? returnElementType = null; // QueryProc type- and regular parameters CodeProperty[]? customRecordProperties = null; AsyncProcedureResult?asyncResult = null; var classes = region.Classes(); // generate custom result type if needed if (storedProcedure.Results.Count > 1) { // TODO: right now we don't have schema API that could load multiple result sets // still it makes sense to implement multiple resultsets generation in future even without // schema API, as user could define resultsets manually throw new NotImplementedException("Multiple result-sets stored procedure generation not imlpemented yet"); } else if (storedProcedure.Results.Count == 1) { // for stored procedure call with result set we use QueryProc API (customTable, var mappedTable, asyncResult) = storedProcedure.Results[0]; // if procedure result table contains unique and not empty column names, we use columns mappings // otherwise we should bind columns manually by ordinal (as we don't have by-ordinal mapping conventions support) useOrdinalMapping = customTable != null // number of columns remains same after empty names and duplicates removed? && customTable.Columns.Select(c => c.Metadata.Name).Where(_ => !string.IsNullOrEmpty(_)).Distinct().Count() != customTable.Columns.Count; if (customTable != null) { (returnElementType, customRecordProperties) = BuildCustomResultClass(customTable, classes, !useOrdinalMapping); } else if (mappedTable != null) { returnElementType = _entityBuilders[mappedTable].Type.Type; } } if (_options.DataModel.GenerateProcedureSync) { BuildStoredProcedureMethod(storedProcedure, dataContextType, methodsGroup, useOrdinalMapping, customTable, returnElementType, customRecordProperties, classes, null, false); } if (_options.DataModel.GenerateProcedureAsync) { BuildStoredProcedureMethod(storedProcedure, dataContextType, methodsGroup, useOrdinalMapping, customTable, returnElementType, customRecordProperties, classes, asyncResult, true); } }