/// <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
        }
Ejemplo n.º 2
0
        /// <summary>
        /// Generates association extension method in extensions class for one side of association.
        /// </summary>
        /// <param name="extensionEntityAssociations">Association methods groupsfor each entity.</param>
        /// <param name="extensionAssociations">Association extensions region provider.</param>
        /// <param name="thisEntity">Entity class for this side of assocation (used for extension <c>this</c> parameter).</param>
        /// <param name="resultEntity">Entity class for other side of assocation (used for extension result type).</param>
        /// <param name="type">Association result type.</param>
        /// <param name="extensionModel">Extension method model.</param>
        /// <param name="metadata">Association methodo metadata.</param>
        /// <param name="associationModel">Association model.</param>
        /// <param name="backReference">Identifies current side of assocation.</param>
        private void BuildAssociationExtension(
            Dictionary <EntityModel, MethodGroup> extensionEntityAssociations,
            Func <RegionGroup> extensionAssociations,
            ClassBuilder thisEntity,
            ClassBuilder resultEntity,
            IType type,
            MethodModel extensionModel,
            AssociationMetadata metadata,
            AssociationModel associationModel,
            bool backReference)
        {
            // create (if missing) assocations region for specific owner (this) entity
            var key = backReference ? associationModel.Target : associationModel.Source;

            if (!extensionEntityAssociations.TryGetValue(key, out var associations))
            {
                extensionEntityAssociations.Add(
                    key,
                    associations = extensionAssociations()
                                   .New(string.Format(EXTENSIONS_ENTITY_ASSOCIATIONS_REGION, thisEntity.Type.Name.Name))
                                   .Methods(false));
            }

            // define extension method
            var methodBuilder = DefineMethod(associations, extensionModel).Returns(type);

            // and it's metadata
            _metadataBuilder.BuildAssociationMetadata(metadata, methodBuilder);

            // build method parameters...
            var thisParam = AST.Parameter(thisEntity.Type.Type, AST.Name(EXTENSIONS_ENTITY_THIS_PARAMETER), CodeParameterDirection.In);
            var ctxParam  = AST.Parameter(WellKnownTypes.LinqToDB.IDataContext, AST.Name(EXTENSIONS_ENTITY_CONTEXT_PARAMETER), CodeParameterDirection.In);

            methodBuilder.Parameter(thisParam);
            methodBuilder.Parameter(ctxParam);

            // ... and body
            if (associationModel.FromColumns == null || associationModel.ToColumns == null)
            {
                // association doesn't specify relation columns (e.g. defined usign expression method)
                // so we should generate exception for non-query execution
                methodBuilder
                .Body()
                .Append(
                    AST.Throw(
                        AST.New(
                            WellKnownTypes.System.InvalidOperationException,
                            AST.Constant(EXCEPTION_QUERY_ONLY_ASSOCATION_CALL, true))));
            }
            else
            {
                // generate association query for non-query invocation

                // As method body here could conflict with custom return type for many-to-one assocation
                // we forcebly override return type here to IQueryable<T>
                if (associationModel.ManyToOne && backReference)
                {
                    methodBuilder.Returns(WellKnownTypes.System.Linq.IQueryable(resultEntity.Type.Type));
                }

                var lambdaParam = AST.LambdaParameter(AST.Name(EXTENSIONS_ASSOCIATION_FILTER_PARAMETER), resultEntity.Type.Type);

                // generate assocation key columns filter, which compare
                // `this` entity parameter columns with return table entity columns
                var fromObject = backReference ? lambdaParam : thisParam;
                var toObject   = !backReference ? lambdaParam : thisParam;

                ICodeExpression?filter = null;

                for (var i = 0; i < associationModel.FromColumns.Length; i++)
                {
                    var fromColumn = _columnProperties[associationModel.FromColumns[i]];
                    var toColumn   = _columnProperties[associationModel.ToColumns[i]];

                    var cond = AST.Equal(
                        AST.Member(fromObject.Reference, fromColumn.Reference),
                        AST.Member(toObject.Reference, toColumn.Reference));

                    filter = filter == null ? cond : AST.And(filter, cond);
                }

                // generate filter lambda function
                var filterLambda = AST
                                   .Lambda(
                    WellKnownTypes.System.Linq.Expressions.Expression(
                        WellKnownTypes.System.Func(WellKnownTypes.System.Boolean, resultEntity.Type.Type)),
                    true)
                                   .Parameter(lambdaParam);
                filterLambda.Body().Append(AST.Return(filter !));

                // ctx.GetTable<ResultEntity>()
                var body = AST.ExtCall(
                    WellKnownTypes.LinqToDB.DataExtensions,
                    WellKnownTypes.LinqToDB.DataExtensions_GetTable,
                    WellKnownTypes.LinqToDB.ITable(resultEntity.Type.Type),
                    new[] { resultEntity.Type.Type },
                    false,
                    ctxParam.Reference);

                // append First/FirstOrDefault (for optional association)
                // for non-many relation
                if (!backReference || !associationModel.ManyToOne)
                {
                    var optional = backReference ? associationModel.TargetMetadata.CanBeNull : associationModel.SourceMetadata.CanBeNull;

                    // .First(t => t.PK == thisEntity.PK)
                    // or
                    // .FirstOrDefault(t => t.PK == thisEntity.PK)
                    body = AST.ExtCall(
                        WellKnownTypes.System.Linq.Queryable,
                        optional ? WellKnownTypes.System.Linq.Queryable_FirstOrDefault : WellKnownTypes.System.Linq.Queryable_First,
                        resultEntity.Type.Type.WithNullability(optional),
                        new[] { resultEntity.Type.Type },
                        true,
                        body,
                        filterLambda.Method);
                }
                else
                {
                    // .Where(t => t.PK == thisEntity.PK)
                    body = AST.ExtCall(
                        WellKnownTypes.System.Linq.Queryable,
                        WellKnownTypes.System.Linq.Queryable_Where,
                        WellKnownTypes.System.Linq.IQueryable(resultEntity.Type.Type),
                        new [] { resultEntity.Type.Type },
                        true,
                        body,
                        filterLambda.Method);
                }

                methodBuilder.Body().Append(AST.Return(body));
            }
        }
Ejemplo n.º 3
0
        /// <summary>
        /// Generates sync or async  stored procedure mapping method.
        /// </summary>
        /// <param name="storedProcedure">Stored procedure model.</param>
        /// <param name="dataContextType">Data context class type.</param>
        /// <param name="methodsGroup">Method group to add new mapping method.</param>
        /// <param name="useOrdinalMapping">If <c>true</c>, by-ordinal mapping used for result mapping instead of by-name mapping.</param>
        /// <param name="customTable">Custom result record model.</param>
        /// <param name="returnElementType">Type of result record for procedure with result.</param>
        /// <param name="customRecordProperties">Column properties for custom result record type.</param>
        /// <param name="classes">Procedure classes group.</param>
        /// <param name="asyncResult">Optional result class model for async signature.</param>
        /// <param name="async">If <c>true</c>, generate async version of mapping.</param>
        private void BuildStoredProcedureMethod(
            StoredProcedureModel storedProcedure,
            IType dataContextType,
            MethodGroup methodsGroup,
            bool useOrdinalMapping,
            ResultTableModel?customTable,
            IType?returnElementType,
            CodeProperty[]?customRecordProperties,
            ClassGroup classes,
            AsyncProcedureResult?asyncResult,
            bool async)
        {
            var hasParameters = storedProcedure.Parameters.Count > 0 || storedProcedure.Return != null;

            // generate ToList materialization call or mark method async in two cases:
            // - when return type of mapping is List<T>
            // - when procedure has non-input parameters
            var toListRequired        = _options.DataModel.GenerateProcedureResultAsList && storedProcedure.Results.Count == 1 && (storedProcedure.Results[0].Entity != null || storedProcedure.Results[0].CustomTable != null);
            var toListOrAsyncRequired = toListRequired ||
                                        storedProcedure.Return != null ||
                                        storedProcedure.Parameters.Any(p => p.Parameter.Direction != CodeParameterDirection.In);

            // declare mapping method
            var method = DefineMethod(
                methodsGroup,
                storedProcedure.Method,
                async,
                async && toListOrAsyncRequired);

            // declare data context parameter (extension `this` parameter)
            var ctxParam = AST.Parameter(
                // see method notes above regarding type of this parameter
                dataContextType,
                AST.Name(STORED_PROCEDURE_CONTEXT_PARAMETER),
                CodeParameterDirection.In);

            method.Parameter(ctxParam);
            var body = method.Body();

            // array of procedure parameters (DataParameter objects)
            CodeVariable?parametersVar = null;

            CodeAssignmentStatement[]?               parameterRebinds = null;
            Dictionary <FunctionParameterModel, int>?rebindedParametersIndexes = null;

            if (hasParameters)
            {
                var resultParametersCount = storedProcedure.Parameters.Count(p => p.Parameter.Direction != CodeParameterDirection.In) + (storedProcedure.Return != null ? 1 : 0);
                // bindings of parameter values to output parameters of method after procedure call
                parameterRebinds = resultParametersCount > 0 ? new CodeAssignmentStatement[resultParametersCount] : Array <CodeAssignmentStatement> .Empty;
                var rebindIndex = 0;
                if (resultParametersCount > 0)
                {
                    rebindedParametersIndexes = new(resultParametersCount);
                }

                // DataParameter collection initialization
                var parameterValues = new ICodeExpression[storedProcedure.Parameters.Count + (storedProcedure.Return != null ? 1 : 0)];
                parametersVar = AST.Variable(AST.Name(STORED_PROCEDURE_PARAMETERS_VARIABLE), WellKnownTypes.LinqToDB.Data.DataParameterArray, true);

                // build non-return parameters
                for (var i = 0; i < storedProcedure.Parameters.Count; i++)
                {
                    var     p              = storedProcedure.Parameters[i];
                    ILValue?rebindTo       = null;
                    var     rebindRequired = p.Parameter.Direction != CodeParameterDirection.In;

                    CodeParameter param;
                    if (async && p.Parameter.Direction != CodeParameterDirection.In)
                    {
                        param = DefineParameter(method, p.Parameter.WithDirection(CodeParameterDirection.In));
                    }
                    else
                    {
                        param = DefineParameter(method, p.Parameter);
                    }

                    if (rebindRequired)
                    {
                        rebindTo = param.Reference;
                    }

                    parameterValues[i] = BuildProcedureParameter(
                        param,
                        param.Type.Type,
                        p.Direction,
                        rebindTo,
                        p.DbName,
                        p.DataType,
                        p.Type,
                        parametersVar,
                        i,
                        parameterRebinds !,
                        rebindIndex);

                    if (p.Parameter.Direction != CodeParameterDirection.In)
                    {
                        rebindedParametersIndexes !.Add(p, rebindIndex);
                        rebindIndex++;
                    }
                }

                // build return parameter
                if (storedProcedure.Return != null)
                {
                    CodeParameter?param = null;
                    if (!async)
                    {
                        param = DefineParameter(method, storedProcedure.Return.Parameter);
                    }

                    parameterValues[storedProcedure.Parameters.Count] = BuildProcedureParameter(
                        param,
                        storedProcedure.Return.Parameter.Type,
                        System.Data.ParameterDirection.ReturnValue,
                        param?.Reference ?? AST.Variable(AST.Name("fake"), storedProcedure.Return.Parameter.Type, false).Reference,
                        storedProcedure.Return.DbName ?? STORED_PROCEDURE_DEFAULT_RETURN_PARAMETER,
                        storedProcedure.Return.DataType,
                        storedProcedure.Return.Type,
                        parametersVar,
                        storedProcedure.Parameters.Count,
                        parameterRebinds !,
                        rebindIndex);
                    rebindedParametersIndexes !.Add(storedProcedure.Return, rebindIndex);
                }

                var parametersArray = AST.Assign(
                    parametersVar,
                    AST.Array(WellKnownTypes.LinqToDB.Data.DataParameter, true, false, parameterValues));
                body.Append(parametersArray);
            }

            CodeParameter?cancellationTokenParameter = null;

            if (async)
            {
                cancellationTokenParameter = DefineParameter(
                    method,
                    new ParameterModel(CANCELLATION_TOKEN_PARAMETER, WellKnownTypes.System.Threading.CancellationToken, CodeParameterDirection.In),
                    AST.Default(WellKnownTypes.System.Threading.CancellationToken, true));
            }

            ICodeExpression?returnValue = null;

            IType returnType;

            if (storedProcedure.Results.Count == 0 || (storedProcedure.Results.Count == 1 && storedProcedure.Results[0].CustomTable == null && storedProcedure.Results[0].Entity == null))
            {
                // for stored procedure call without result set we use ExecuteProc API
                // prepare call parameters
                var parametersCount       = (hasParameters ? 3 : 2) + (async ? 1 : 0);
                var executeProcParameters = new ICodeExpression[parametersCount];
                executeProcParameters[0] = ctxParam.Reference;
                executeProcParameters[1] = AST.Constant(BuildFunctionName(storedProcedure.Name), true);
                if (async)
                {
                    executeProcParameters[2] = cancellationTokenParameter !.Reference;
                }
                if (hasParameters)
                {
                    executeProcParameters[async ? 3 : 2] = parametersVar !.Reference;
                }

                returnType = WellKnownTypes.System.Int32;
                if (async)
                {
                    returnValue = AST.ExtCall(
                        WellKnownTypes.LinqToDB.Data.DataConnectionExtensions,
                        WellKnownTypes.LinqToDB.Data.DataConnectionExtensions_ExecuteProcAsync,
                        WellKnownTypes.System.Int32,
                        executeProcParameters);

                    if (asyncResult != null)
                    {
                        var rowCountVar = AST.Variable(
                            AST.Name(STORED_PROCEDURE_RESULT_VARIABLE),
                            WellKnownTypes.System.Int32,
                            true);
                        body.Append(AST.Assign(rowCountVar, AST.AwaitExpression(returnValue)));
                        returnValue = rowCountVar.Reference;
                    }
                }
                else
                {
                    returnValue = AST.ExtCall(
                        WellKnownTypes.LinqToDB.Data.DataConnectionExtensions,
                        WellKnownTypes.LinqToDB.Data.DataConnectionExtensions_ExecuteProc,
                        WellKnownTypes.System.Int32,
                        executeProcParameters);
                }
            }
            else if (storedProcedure.Results.Count == 1)
            {
                // QueryProc type- and regular parameters
                IType[]           queryProcTypeArgs;
                ICodeExpression[] queryProcParameters;

                if (useOrdinalMapping)
                {
                    // for ordinal mapping we call QueryProc API with mapper lambda parameter:
                    // manual mapping
                    // Example:

                    /*
                     * dataReader => new CustomResult()
                     * {
                     *     Column1 = Converter.ChangeTypeTo<int>(dataReader.GetValue(0), dataConnection.MappingSchema),
                     *     Column2 = Converter.ChangeTypeTo<string>(dataReader.GetValue(1), dataConnection.MappingSchema),
                     * }
                     */

                    queryProcParameters = new ICodeExpression[(hasParameters ? 4 : 3) + (async ? 1 : 0)];

                    // generate positional mapping lambda
                    // TODO: switch to ColumnReader.GetValue in future to utilize more precise mapping
                    // based on column mapping attributes
                    var drParam = AST.LambdaParameter(
                        AST.Name(STORED_PROCEDURE_CUSTOM_MAPPER_PARAMETER),
                        // TODO: add IDataReader support here for linq2db v3
                        WellKnownTypes.System.Data.Common.DbDataReader);
                    var initializers = new CodeAssignmentStatement[customTable !.Columns.Count];