Question

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);
        }
Was it helpful?

Solution

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

OTHER TIPS

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

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!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top