/// <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); } }
/// <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); } } } }
/// <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];