这样做的主要问题是源属性和目标属性之间没有关系。它们唯一的关系是选择中的赋值。更复杂的是,源类型和目标类型之间的关系可以根据 select 表达式进行更改,因此任何映射都必须针对每个 select 表达式。
建议的解决方案
向任何结果类型添加一个字段,为属性的映射指定一个 if。该字段将是一个简单的int 常量,添加到选择中,用于索引列表以查找适当的映射。
创建Select 表达式时,将在表达式中搜索属性映射并记录下来。该映射将添加一个映射的全局列表及其在选择中使用的索引,如果多次使用相同的映射,则使用相同的索引。
表示映射的字段可以通过两种方式初始化:
- 使用
GenerateMapId 方法显式设置字段。方法调用将替换为表示地图索引的常量。
- 实现
IWithMetadataMapping 接口。在这种情况下,MapId 属性和表示地图索引的常量之间的绑定将被添加到 Select 表达式中
用法
var result = c.Entities
.WithAttributeProjection(e => new
{
MapId = Extensions.GenerateMapId(), // We need mapping for this type
Prop1 = e.Id,
Prop2 = e.Name,
Prop3 = e.Child.Name,
Nested = new
{
MapId = Extensions.GenerateMapId(), // Also for this type
Prop1 = e.Id,
Prop2 = e.Name,
Prop3 = e.Child.Name,
Children = e.ManyChild.Select(m => new WithMetadataMapping // This implements IWithMetadataMapping so it will get the mappgin automatically
{
Name = m.Name
})
}
});
foreach (var item in result)
{
var map = Extensions.GetMapping(item.MapId); // Get the map for the outer new
var type = item.GetType();
var prop2 = type.GetProperty(nameof(item.Prop2));
var sourceProp2 = map.GetSourceMember(prop2);
var value2 = sourceProp2.GetCustomAttribute<CustomAttribute1Attribute>().Value;
Console.WriteLine(value2); // Entity.Name
var prop3 = type.GetProperty(nameof(item.Prop3));
var sourceProp3 = map.GetSourceMember(prop3);
var value3 = sourceProp3.GetCustomAttribute<CustomAttribute1Attribute>().Value;
Console.WriteLine(value3); // ChildEntity.Name
foreach (var child in item.Nested.Children)
{
var childMap = Extensions.GetMapping(child.MapId);
var nameProp = child.GetType().GetProperty(nameof(child.Name));
var value = childMap.GetSourceMember(nameProp).GetCustomAttribute<CustomAttribute1Attribute>().Value;
Console.WriteLine(value); // ManyChildEntity.Name
}
}
实施
interface IWithMetadataMapping
{
int MapId { get; set; }
}
public static class Extensions
{
private static MethodInfo GenerateMapIdMethodInfo = typeof(Extensions).GetMethod(nameof(GenerateMapId));
public static int GenerateMapId()
{
throw new NotSupportedException("Should not be invoked directly");
}
static Dictionary<Mapping, int> MappingLookup = new Dictionary<Mapping, int>();
static List<Mapping> Mappings = new List<Mapping>();
public static Mapping GetMapping(int index)
{
return Mappings[index];
}
public class Mapping
{
public Mapping(Dictionary<MemberInfo, MemberInfo[]> propertyMappings)
{
this.propertyMappings = propertyMappings;
int hc = propertyMappings.Count;
foreach (var kv in propertyMappings)
{
hc = unchecked(hc * 314159 + kv.Key.GetHashCode());
hc = unchecked(hc * 314159 + kv.Value.Length);
foreach (var pi in kv.Value)
{
hc = unchecked(hc * 314159 + pi.GetHashCode());
}
}
this.hashCode = hc;
}
private Dictionary<MemberInfo, MemberInfo[]> propertyMappings;
private int hashCode;
public override int GetHashCode() => this.hashCode;
public override bool Equals(object obj)
{
if(obj is Mapping map)
{
return map.propertyMappings.Count == this.propertyMappings.Count &&
map.propertyMappings.Keys.SequenceEqual(this.propertyMappings.Keys) &&
map.propertyMappings.Values
.Zip(this.propertyMappings.Values, (a, b) => a.SequenceEqual(b))
.All(v => v);
}
return false;
}
public MemberInfo[] GetSourceMemberChain(MemberInfo pi)
{
return this.propertyMappings[pi];
}
public bool TryGetSourceMemberChain(MemberInfo pi, out MemberInfo[] source)
{
return this.propertyMappings.TryGetValue(pi, out source) ;
}
public MemberInfo GetSourceMember(MemberInfo pi)
{
return this.GetSourceMemberChain(pi).First();
}
public bool TryGetSourceMember(MemberInfo pi, out MemberInfo source)
{
if(this.propertyMappings.TryGetValue(pi, out var sources))
{
source = sources.First();
return true;
}
else
{
source = null;
return false;
}
}
}
public static IQueryable<TResult> WithAttributeProjection<TSource, TResult>(this IQueryable<TSource> @this, Expression<Func<TSource, TResult>> selector)
{
var visitor = new MapExtractionVisitor();
var newExpression = (Expression<Func<TSource, TResult>>)visitor.Visit(selector);
return @this.Select(newExpression);
}
public class MapExtractionVisitor : ExpressionVisitor
{
List<MemberInfo> capturedMemebers;
bool capturePropAccess = false;
protected void CreateMapping(int count,
Func<int, Expression> getValue,
Action<int, Expression> setValue,
Func<int, MemberInfo> getMemeber,
Func<int, bool> isGenerateMapId,
bool alwaysExtractMap,
out bool isDirty)
{
var mappingIdMember = -1;
isDirty = false;
Dictionary<MemberInfo, MemberInfo[]> propertyMappings = new Dictionary<MemberInfo, MemberInfo[]>();
for (int index = 0; index < count; index++)
{
if (isGenerateMapId(index))
{
mappingIdMember = index;
}
var arg = getValue(index);
if (arg == null) continue;
var originalCapturePropAccess = this.capturePropAccess;
var originalCapturedMemebers = this.capturedMemebers;
this.capturePropAccess = true;
this.capturedMemebers = new List<MemberInfo>();
var newArgument = this.Visit(arg);
setValue(index, newArgument);
isDirty = isDirty || newArgument != arg;
if (this.capturedMemebers.Any())
{
propertyMappings.Add(getMemeber(index), this.capturedMemebers.ToArray());
}
this.capturedMemebers = originalCapturedMemebers;
this.capturePropAccess = originalCapturePropAccess;
}
if (mappingIdMember != -1 || alwaysExtractMap)
{
lock (MappingLookup)
{
var newMapping = new Mapping(propertyMappings);
if (!MappingLookup.TryGetValue(newMapping, out var mapId))
{
mapId = Mappings.Count;
Mappings.Add(newMapping);
MappingLookup.Add(newMapping, mapId);
}
setValue(mappingIdMember, Expression.Constant(mapId));
}
isDirty = true;
}
}
protected override Expression VisitNew(NewExpression node)
{
if (node.Members != null)
{
Expression[] newArguments = new Expression[node.Arguments.Count];
CreateMapping(node.Arguments.Count,
i => node.Arguments[i],
(i, e) => newArguments[i] = e,
i => node.Members[i],
i => node.Arguments[i] is MethodCallExpression mArg && mArg.Method == GenerateMapIdMethodInfo,
false,
out var isDirty);
if (isDirty)
{
return node.Update(newArguments);
}
else
{
return node;
}
}
else
{
return base.VisitNew(node);
}
}
protected override Expression VisitMemberInit(MemberInitExpression node)
{
Expression[] newArguments = new Expression[node.Bindings.Count + 1];
var bindings = node.Bindings;
var implementsInterface = typeof(IWithMetadataMapping).IsAssignableFrom(node.Type);
CreateMapping(bindings.Count,
i => bindings[i] is MemberAssignment ma ? ma.Expression : null,
(i, e) => newArguments[i != -1 ? i : (newArguments.Length - 1)] = e,
i => bindings[i] is MemberAssignment ma ? ma.Member : null,
i =>
{
if (bindings[i] is MemberAssignment ma)
{
if (ma.Expression is MethodCallExpression mArg && mArg.Method == GenerateMapIdMethodInfo)
{
return true;
}
}
return false;
},
implementsInterface,
out var isDirty);
if (isDirty)
{
int count = node.Bindings.Count;
bool addBinding = newArguments.Last() != null;
var inits = new MemberBinding[count + (addBinding ? 1 : 0)];
for (int i = 0; i < count; i++)
{
var binding = node.Bindings[i];
if (newArguments[i] == null)
{
inits[i] = binding;
}
else if(binding is MemberAssignment m)
{
inits[i] = Expression.Bind(m.Member, newArguments[i]);
}
}
if(addBinding)
{
var mapProp = node.Type.GetProperty(nameof(IWithMetadataMapping.MapId));
inits[inits.Length - 1] = Expression.Bind(mapProp, newArguments.Last());
}
return node.Update(node.NewExpression, inits);
}
return base.VisitMemberInit(node);
}
public override Expression Visit(Expression node)
{
// We only capture an interupted chanin of member accesses
if(capturePropAccess && node is MemberExpression memberExpression)
{
capturedMemebers.Add(memberExpression.Member);
}
else
{
capturePropAccess = false;
}
return base.Visit(node);
}
}
}
注意我没有对此进行广泛的测试,因此可能存在错误,如果您想使用该解决方案,并且遇到问题我可以帮助解决它们。