2

I have the following class, for which usage is not important. What is important is method SetCacheItemSelector which takes one parameter, a select expression that projects Account entity to AccountCacheDTO:

public class AccountRepositoryCache : RepositoryCache<Account, AccountCacheDTO>
{
    public AccountRepositoryCache()
    {
        SetCacheItemSelector(x => new AccountCacheDTO
        {
            Id = x.Id,
            Login = x.Login
        });
    }
}

So signature for this method is:

public void SetCacheItemSelector(Expression<Func<TEntity, TCacheItem>> selector)

In this case, TEntity is Account class, and TCacheItem is AccountCacheDTO class.

Is there a way to use reflection to build select expression dynamically for all the properties that are matching for both Account class and AccountCacheDTO class?

Goal is to have method that would look like this:

public Expression<Func<TEntity, TCacheItem>> BuildSelector<TEntity, TCacheItem>()
{
... // implementation with reflection goes here
}

EDIT:

Here is final implementation (pretty much the same as the accepted answer):

public static Expression<Func<TSource, TTarget>> BuildSelector<TSource, TTarget>()
        {
            Type targetType = typeof(TTarget);
            Type sourceType = typeof(TSource);
            ParameterExpression parameterExpression = Expression.Parameter(sourceType, "source");
            List<MemberBinding> bindings = new List<MemberBinding>();
            foreach (PropertyInfo sourceProperty in sourceType.GetProperties().Where(x => x.CanRead))
            {
                PropertyInfo targetProperty = targetType.GetProperty(sourceProperty.Name);
                if (targetProperty != null && targetProperty.CanWrite && targetProperty.PropertyType.IsAssignableFrom(sourceProperty.PropertyType))
                {
                    MemberExpression propertyExpression = Expression.Property(parameterExpression, sourceProperty);
                    bindings.Add(Expression.Bind(targetProperty, propertyExpression));
                }
            }
            NewExpression newExpression = Expression.New(targetType);
            Expression initializer = Expression.MemberInit(newExpression, bindings);
            return Expression.Lambda<Func<TSource, TTarget>>(initializer, parameterExpression);
        }
Admir Tuzović
  • 10,997
  • 7
  • 35
  • 71

3 Answers3

2

I didn't test it, but you should be able to do something like: This is just to convey a general idea and you should be able to tweak it for your requirements.

    public Expression<Func<TEntity, TCacheItem>> BuildSelector<TEntity, TCacheItem>(TEntity entity)
    {
        List<MemberBinding> memberBindings = new List<MemberBinding>();
        MemberInitExpression body = null;

        foreach (var entityPropertyInfo in typeof(TEntity).GetProperties())
        {
            foreach (var cachePropertyInfo in typeof(TCacheItem).GetProperties())
            {
                if (entityPropertyInfo.PropertyType == cachePropertyInfo.PropertyType && entityPropertyInfo.Name == cachePropertyInfo.Name)
                {
                    var fieldExpressoin = Expression.Field(Expression.Constant(entity), entityPropertyInfo.Name);
                    memberBindings.Add(Expression.Bind(cachePropertyInfo, fieldExpressoin));
                }
            }
        }

        var parameterExpression = Expression.Parameter(typeof(TEntity), "x");
        var newExpr = Expression.New(typeof(TCacheItem));
        body = Expression.MemberInit(newExpr, memberBindings);
        return Expression.Lambda<Func<TEntity, TCacheItem>>(body, parameterExpression);
    }
Shalin Ved
  • 322
  • 1
  • 6
  • The expression which is generated a little different as required in OP as well as method signature. – Hamlet Hakobyan Apr 29 '14 at 19:40
  • @HamletHakobyan Ofcourse it is. I didn't test it but the answer was to convey a general idea which could be tweaked as per OP's requirement. – Shalin Ved Apr 29 '14 at 19:51
1

Of course, the @Aravol's answer can make sense, but it is a little different which required in OP. Here is the solution which is more suitable to OP requirement.

public Expression<Func<TEntity, TCacheItem>> BuildSelector<TEntity, TCacheItem>()
{
    Type type = typeof(TEntity);
    Type typeDto = typeof(TCacheItem);
    var ctor = Expression.New(typeDto);
    ParameterExpression parameter = Expression.Parameter(type, "p");
    var propertiesDto = typeDto.GetProperties(BindingFlags.Public | BindingFlags.Instance);
    var memberAssignments = propertiesDto.Select(p =>
    {
        PropertyInfo propertyInfo = type.GetProperty(p.Name, BindingFlags.Public | BindingFlags.Instance);
        MemberExpression memberExpression = Expression.Property(parameter, propertyInfo);
        return Expression.Bind(p, memberExpression);
    });
    var memberInit = Expression.MemberInit(ctor, memberAssignments);
    return Expression.Lambda<Func<TEntity, TCacheItem>>(memberInit, parameter);
}
Hamlet Hakobyan
  • 32,965
  • 6
  • 52
  • 68
0

Your best bet is to get very comfortable with the System.Linq.Expressions namespace, which contains all of the methods you'll need to dynamically metacode your method calls and compile them into delegates. See especially Expression.Call and Lambda.Compile methods. Note that using Lambda.Compile, you can also have a true, compiled Delegate, instead of an expression tree (Expression) wrapping the call to your desired method. (NOTE: You can also forgo the Compile step if you really want that expression tree for later)

As for building your set, that's Assembly scanning, and is going to be a matter of iterating over all classes in your Assembly. I highly recommend you utilize, at the very least, a custom Attribute on your assembly or future assemblies to mark them for this scan, lest this process end up much more costly. At the most, you should consider using a custom Attribute to mark which properties you want scanned for this expression build.

the actual code to this tends to start with

AppDomain.CurrentDomain // Necessary to get all available Assemblies
    .GetAssemblies()    // Gets all the assemblies currently loaded in memory that this code can work with
    .AsParallel()       // Highly recommended to make the attribute-checking steps run asynchronously
                        // Also gives you a handy .ForAll Method at the end
    // TODO: .Where Assembly contains Attribute
    .SelectMany(assembly => assembly.GetTypes())
    // TODO: .Where Type contains Attribute
    .SelectMany(type => type.GetProperties)
    // TODO: Make sure Property has the right data...
    .Select(CompileFromProperty)

Where CompileFromProperty is a method taking PropertyInfo and returning the desired Expression.

Look into ToList() and ToDictionary after that, as you may need to break out of the parallelization once you start pushing values to your cache

Addendum: you also have .MakeGenericType on the Type class, which will allow you to specify Generic parameters from other Type variables, which will prove invaluable when building the Expressions. Don't forget about Contravariance when you define the generic types!

David
  • 10,458
  • 1
  • 28
  • 40
  • This doesn't actually answer the question. The implementation of the actual method requested in the question is simply left as a "TODO" comment here. It's that TODO that he's specifically asking for help with. – Servy Apr 29 '14 at 18:47
  • Actually, my sample code is only regarding the Assembly scanning, which is how to find all matching properties. The actual implementation of how to build the expressions from reflection is where the Expressions Namespace comes in – David Apr 29 '14 at 18:49
  • @Aravol I'm having a hard time understanding how your code relates to my question... – Admir Tuzović Apr 29 '14 at 19:06
  • The code I posted only regards scanning assemblies, which can get pretty hairy itself. The actual answer is the mention of Expression.Call function – David Apr 29 '14 at 19:08
  • @Aravol, sorry but why would I scan assemblies in the first place? – Admir Tuzović Apr 29 '14 at 19:12
  • You mentioned needing to find for "all the properties matching the type" so I included the mentioned (and, admittedly, tangent) of assembly scanning, that way you can at any time add in such properties, and they'll be registered at runtime simply by being defined – David Apr 29 '14 at 20:38