/// <summary> /// Compiles methods to access the data required for the DML operation /// </summary> /// <param name="mappings">The mappings of attribute name to source column</param> /// <param name="schema">The schema of data source</param> /// <param name="attributes">The attributes in the target metadata</param> /// <param name="dateTimeKind">The time zone that datetime values are supplied in</param> /// <returns></returns> protected Dictionary <string, Func <Entity, object> > CompileColumnMappings(EntityMetadata metadata, IDictionary <string, string> mappings, INodeSchema schema, IDictionary <string, AttributeMetadata> attributes, DateTimeKind dateTimeKind) { var attributeAccessors = new Dictionary <string, Func <Entity, object> >(); var entityParam = Expression.Parameter(typeof(Entity)); foreach (var mapping in mappings) { var sourceColumnName = mapping.Value; var destAttributeName = mapping.Key; if (!schema.ContainsColumn(sourceColumnName, out sourceColumnName)) { throw new QueryExecutionException($"Missing source column {mapping.Value}") { Node = this } } ; // We might be using a virtual ___type attribute that has a different name in the metadata. We can safely // ignore these attributes - the attribute names have already been validated in the ExecutionPlanBuilder if (!attributes.TryGetValue(destAttributeName, out var attr) || attr.AttributeOf != null) { continue; } var sourceType = schema.Schema[sourceColumnName]; var destType = attr.GetAttributeType(); var destSqlType = SqlTypeConverter.NetToSqlType(destType); var expr = (Expression)Expression.Property(entityParam, typeof(Entity).GetCustomAttribute <DefaultMemberAttribute>().MemberName, Expression.Constant(sourceColumnName)); var originalExpr = expr; if (sourceType == typeof(object)) { // null literal expr = Expression.Constant(null, destType); expr = Expr.Box(expr); } else { expr = SqlTypeConverter.Convert(expr, sourceType); expr = SqlTypeConverter.Convert(expr, destSqlType); var convertedExpr = SqlTypeConverter.Convert(expr, destType); if (attr is LookupAttributeMetadata lookupAttr && lookupAttr.AttributeType != AttributeTypeCode.PartyList) { // Special case: intersect attributes can be simple guids if (metadata.IsIntersect != true) { if (sourceType == typeof(SqlEntityReference)) { expr = SqlTypeConverter.Convert(originalExpr, sourceType); convertedExpr = SqlTypeConverter.Convert(expr, typeof(EntityReference)); } else { Expression targetExpr; if (lookupAttr.Targets.Length == 1) { targetExpr = Expression.Constant(lookupAttr.Targets[0]); } else { var sourceTargetColumnName = mappings[destAttributeName + "type"]; var sourceTargetType = schema.Schema[sourceTargetColumnName]; targetExpr = Expression.Property(entityParam, typeof(Entity).GetCustomAttribute <DefaultMemberAttribute>().MemberName, Expression.Constant(sourceTargetColumnName)); targetExpr = SqlTypeConverter.Convert(targetExpr, sourceTargetType); targetExpr = SqlTypeConverter.Convert(targetExpr, typeof(SqlString)); targetExpr = SqlTypeConverter.Convert(targetExpr, typeof(string)); } convertedExpr = Expression.New( typeof(EntityReference).GetConstructor(new[] { typeof(string), typeof(Guid) }), targetExpr, Expression.Convert(convertedExpr, typeof(Guid)) ); } destType = typeof(EntityReference); } } else if (attr is EnumAttributeMetadata && !(attr is MultiSelectPicklistAttributeMetadata)) { convertedExpr = Expression.New( typeof(OptionSetValue).GetConstructor(new[] { typeof(int) }), Expression.Convert(convertedExpr, typeof(int)) ); destType = typeof(OptionSetValue); } else if (attr is MoneyAttributeMetadata) { convertedExpr = Expression.New( typeof(Money).GetConstructor(new[] { typeof(decimal) }), Expression.Convert(expr, typeof(decimal)) ); destType = typeof(Money); } else if (attr is DateTimeAttributeMetadata) { convertedExpr = Expression.Convert( Expr.Call(() => DateTime.SpecifyKind(Expr.Arg <DateTime>(), Expr.Arg <DateTimeKind>()), expr, Expression.Constant(dateTimeKind) ), typeof(DateTime?) ); } // Check for null on the value BEFORE converting from the SQL to BCL type to avoid e.g. SqlDateTime.Null being converted to 1900-01-01 expr = Expression.Condition( SqlTypeConverter.NullCheck(expr), Expression.Constant(null, destType), convertedExpr); if (expr.Type.IsValueType) { expr = SqlTypeConverter.Convert(expr, typeof(object)); } } attributeAccessors[destAttributeName] = Expression.Lambda <Func <Entity, object> >(expr, entityParam).Compile(); }