/// <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
        }
Exemple #2
0
        // 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);
            }
        }