简体   繁体   中英

lambda using tuple via reflection

I have some code to sync Entity Framework POCOs between different database servers (source server is SQL Server, destination is MySQL). It is written to work generically using the [Key] attribute and the fact that the POCOs representing the data being synced know how to compare themselves.

The code is currently as follows:

var srcdbset = setprop.GetValue(src, null); var dstdbset = setprop.GetValue(dst, null);
var tabletype = srcdbset.GetType().GetGenericArguments().First();
var keys = tabletype.GetProperties().Where(p => p.GetCustomAttributesData().FirstOrDefault(a => a.AttributeType.Name == "KeyAttribute") != null).ToList();

var param = Expression.Parameter(tabletype);  // TODO - support compound keys
var dt = typeof(Func<,>).MakeGenericType(tabletype, keys[0].PropertyType);
var df = Expression.Lambda(dt, Expression.Property(param, keys[0].Name), param).Compile();
var qtodict = typeof(Enumerable).GetMethods().Where(m => m.Name == "ToDictionary").First();
var gtodict = qtodict.MakeGenericMethod(new[] { tabletype, keys[0].PropertyType });
var srcdict = ((IDictionary)gtodict.Invoke(null, new object[] { srcdbset, df }));
var dstdict = ((IDictionary)gtodict.Invoke(null, new object[] { dstdbset, df }));
var qexcept = typeof(Enumerable).GetMethods().Where(m => m.Name == "Except").First();
var gexcept = qexcept.MakeGenericMethod(new[] { keys[0].PropertyType });
dynamic snotd = gexcept.Invoke(null, new object[] { srcdict.Keys, dstdict.Keys });
dynamic dnots = gexcept.Invoke(null, new object[] { dstdict.Keys, srcdict.Keys });

A little help - src is the source DbContext and dst is the destination DbContext and setprop is the PropertyInfo object of the DbSet being synced.

This question really isn't so much about Entity Framework, but linq and reflection. As you can see the TODO comment says - "support compound keys" - the code above works fine for POCOs with just one key, but to support composite keys, the lambda expression needs to change from something like:

dbcontext.Accounts.ToDictionary(a => a.AccountID);

to something like:

dbcontext.OntLocations.ToDictionary(l => Tuple.Create(l.OntID, l.LocationID, l.Index);

The two linq expressions immediately above are obviously written in a non generic way to make things simpler to explain - my question is - how do I write a generic lambda that does the tuple create? If anyone could point me in the right direction on that, I think the rest of the code would work as is.

Also, you are probably thinking - why aren't they using transactional replication - long story short - unable to find a product that works reliably - if anyone knows of one that works well from SQL Server to MySQL and doesn't require much/any downtime to the SQL Server to install I'm all ears.

First you will need to get the type of tuple you need to create, this has to be hard-coded per number of keys since the Tuple class is actually a different class depending on the number of items:

Type tupleType = null;
if(keys.Count == 1) tupleType = typeof(Tuple<>);
else if(keys.Count == 2) tupleType = typeof(Tuple<,>);
else if(keys.Count == 3) tupleType = typeof(Tuple<,,>);
else if(keys.Count == 4) tupleType = typeof(Tuple<,,,>);
//and so on

tupleType = tupleType.MakeGenericType(keys.Select(t=>t.PropertyType).ToArray());

Now you can make the Func as you had it above:

var lambdaFuncType = typeof(Func<,>).MakeGenericType(tabletype, tupleType);

Now build the expression to create the tuple. We'll need the tuple constructor and a property accessor expression for each key.

var parameterExpression = Expression.Parameter(tabletype);
var constructorExpression = Expression.New(tupleType.GetConstructors()[0], 
    keys.Select(t=>Expression.Property(parameterExpression, t.Name)).ToArray());

Now make the full lambda and compile it:

var compiledExpression = Expression.Lambda(lambdaFuncType, constructorExpression, parameterExpression).Compile();

The answer by @Dave M is fine, but the whole thing can be greatly simplified by emitting a call to one of the Tuple.Create method overloads using the following handy Expression.Call method overload

public static MethodCallExpression Call(
    Type type,
    string methodName,
    Type[] typeArguments,
    params Expression[] arguments
)

which will do the most of the work for you:

var source = Expression.Parameter(tabletype, "e");
var key = Expression.Call(typeof(Tuple), "Create", 
    keys.Select(pi => pi.PropertyType).ToArray(), // generic type arguments
    keys.Select(pi => Expression.MakeMemberAccess(source, pi)).ToArray() // arguments
);
var keySelector = Expression.Lambda(key, source).Compile();

Later on, key.Type property can be used to get the tuple type.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM