Ejemplo n.º 1
0
        /// <summary>
        /// Generates assocation keys in metadata.
        /// </summary>
        /// <param name="association">Association model.</param>
        /// <param name="backReference">Association side.</param>
        private void BuildAssociationMetadataKey(AssociationModel association, bool backReference)
        {
            // metadata model to update with keys
            var metadata = backReference ? association.TargetMetadata : association.SourceMetadata;

            // if metadata keys already specified in model, skip generation
            // (we don't generate them before this stage, so it means user generated them manually)
            if (metadata.ExpressionPredicate != null ||
                metadata.QueryExpressionMethod != null ||
                metadata.ThisKey != null ||
                metadata.ThisKeyExpression != null ||
                metadata.OtherKey != null ||
                metadata.OtherKeyExpression != null)
            {
                return;
            }

            if (association.FromColumns == null ||
                association.ToColumns == null ||
                association.FromColumns.Length != association.ToColumns.Length ||
                association.FromColumns.Length == 0)
            {
                throw new InvalidOperationException($"Invalid association configuration: association columns missing or mismatch on both sides of assocation.");
            }

            var thisColumns  = backReference ? association.ToColumns : association.FromColumns;
            var otherColumns = !backReference ? association.ToColumns : association.FromColumns;

            var thisBuilder  = !backReference ? _entityBuilders[association.Source] : _entityBuilders[association.Target];
            var otherBuilder = backReference ? _entityBuilders[association.Source] : _entityBuilders[association.Target];

            // we generate keys using nameof operator to get property name to have refactoring-friendly mappings
            // (T4 always used strings here)
            var separator = association.FromColumns.Length > 1 ? AST.Constant(",", true) : null;

            for (var i = 0; i < association.FromColumns.Length; i++)
            {
                if (i > 0)
                {
                    // add comma separator
                    metadata.ThisKeyExpression  = AST.Add(metadata.ThisKeyExpression !, separator !);
                    metadata.OtherKeyExpression = AST.Add(metadata.OtherKeyExpression !, separator !);
                }

                // generate nameof() expressions for current key column
                var thisKey  = AST.NameOf(AST.Member(thisBuilder.Type.Type, _columnProperties[thisColumns [0]].Reference));
                var otherKey = AST.NameOf(AST.Member(otherBuilder.Type.Type, _columnProperties[otherColumns[0]].Reference));

                // append column name to key
                metadata.ThisKeyExpression  = metadata.ThisKeyExpression == null ? thisKey  : AST.Add(metadata.ThisKeyExpression, thisKey);
                metadata.OtherKeyExpression = metadata.OtherKeyExpression == null ? otherKey : AST.Add(metadata.OtherKeyExpression, thisKey);
            }
        }
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));
            }
        }
        /// <summary>
        /// Generates aggregate function mapping.
        /// </summary>
        /// <param name="aggregate">Aggrrgate function model.</param>
        /// <param name="functionsGroup">Functions region.</param>
        private void BuildAggregateFunction(AggregateFunctionModel aggregate, Func <RegionGroup> functionsGroup)
        {
            // generation sample:

            /*
             * [Sql.Function("test_avg", ArgIndices = new []{ 1 }, ServerSideOnly = true, IsAggregate = true)]
             * public static double? TestAvg<TSource>(this IEnumerable<TSource> src, Expression<Func<TSource, double>> value)
             * {
             *     throw new InvalidOperationException("error message here");
             * }
             */
            // where
            // - src/TSource: any aggregated table-like source
            // - value: actual aggregated value (value selector from source)

            var method = DefineMethod(
                functionsGroup().New(aggregate.Method.Name).Methods(false),
                aggregate.Method);

            // aggregates cannot be used outside of query context, so we throw exception from method
            var body = method
                       .Body()
                       .Append(
                AST.Throw(AST.New(
                              WellKnownTypes.System.InvalidOperationException,
                              AST.Constant(EXCEPTION_QUERY_ONLY_ASSOCATION_CALL, true))));

            // build mappings
            _metadataBuilder.BuildFunctionMetadata(aggregate.Metadata, method);

            var source = AST.TypeParameter(AST.Name(AGGREGATE_RECORD_TYPE));

            method.TypeParameter(source);

            method.Returns(aggregate.ReturnType);

            // define parameters
            // aggregate has at least one parameter - collection of aggregated values
            // and optionally could have one or more additional scalar parameters
            var sourceParam = AST.Parameter(
                WellKnownTypes.System.Collections.Generic.IEnumerable(source),
                AST.Name(AGGREGATE_SOURCE_PARAMETER),
                CodeParameterDirection.In);

            method.Parameter(sourceParam);

            if (aggregate.Parameters.Count > 0)
            {
                for (var i = 0; i < aggregate.Parameters.Count; i++)
                {
                    var param         = aggregate.Parameters[i];
                    var parameterType = param.Parameter.Type;

                    // scalar parameters have following type:
                    // Expression<Func<TSource, param_type>>
                    // which allows user to specify aggregated value(s) selection from source record
                    parameterType = WellKnownTypes.System.Linq.Expressions.Expression(
                        WellKnownTypes.System.Func(parameterType, source));

                    var p = AST.Parameter(parameterType, AST.Name(param.Parameter.Name, null, i + 1), CodeParameterDirection.In);
                    method.Parameter(p);

                    if (param.Parameter.Description != null)
                    {
                        method.XmlComment().Parameter(p.Name, param.Parameter.Description);
                    }
                }
            }
        }
Ejemplo n.º 4
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];