简体   繁体   中英

c#: Call boxed delegate

In my project I need to transform data between several classes so I created a class DataMapper that is used for strong-typed mapping of properties from two different classes. When properties in the pair need to be modified I store two delegates (converters) for this purpose.

Then the DataMapper has two methods Update(T source, S target) and Update(S source, T target) that use these mappings to provide the tranformation.

public class DataMapper<TSourceType, TTargetType> : IDataUpdater<TSourceType, TTargetType> {

    private readonly IDictionary<PropertyInfo, PropertyInfo> _sourceToTargetMap = new Dictionary<PropertyInfo, PropertyInfo>();
    private readonly IDictionary<PropertyInfo, object> _converters = new Dictionary<PropertyInfo, object>();

    public DataMapper<TSourceType, TTargetType> Map<TSourceValue, TTargetValue>(
        Expression<Func<TSourceType, TSourceValue>> sourcePropExpr,
        Expression<Func<TTargetType, TTargetValue>> targetPropExpr) 
    {
        _sourceToTargetMap.Add(sourcePropExpr.AsPropertyInfo(), targetPropExpr.AsPropertyInfo());
        return this;
    }

    public DataMapper<TSourceType, TTargetType> Map<TSourceValue, TTargetValue>(
        Expression<Func<TSourceType, TSourceValue>> sourcePropExpr,
        Expression<Func<TTargetType, TTargetValue>> targetPropExpr,
        Func<TSourceValue, TTargetValue> sourceToTargetConverter, 
        Func<TTargetValue, TSourceValue> targetToSourceConverter) 
    {
        _sourceToTargetMap.Add(sourcePropExpr.AsPropertyInfo(), targetPropExpr.AsPropertyInfo());
        _converters.Add(sourcePropExpr.AsPropertyInfo(), sourceToTargetConverter);
        _converters.Add(targetPropExpr.AsPropertyInfo(), targetToSourceConverter);
        return this;
    }

    public void Update(TSourceType source, TTargetType target) {
        foreach (var keyValuePair in _sourceToTargetMap) {
            var sourceProp = keyValuePair.Key;
            var targetProp = keyValuePair.Value;
            Update(source, target, sourceProp, targetProp);
        }
    }

    public void Update(TTargetType source, TSourceType target) {
        foreach (var keyValuePair in _sourceToTargetMap) {
            var sourceProp = keyValuePair.Value;
            var targetProp = keyValuePair.Key;
            Update(source, target, sourceProp, targetProp);
        }
    }

    private void Update(
        object source, 
        object target, 
        PropertyInfo sourceProperty, 
        PropertyInfo targetProperty) 
    {
        var sourceValue = sourceProperty.GetValue(source);
        if (_converters.ContainsKey(sourceProperty)) {
            sourceValue = typeof(InvokeHelper<,>)
                .MakeGenericType(sourceProperty.PropertyType, targetProperty.PropertyType)
                .InvokeMember("Call", BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod, null, null, new[] { _converters[sourceProperty], sourceValue });
        }
        targetProperty.SetValue(target, sourceValue);
    }
}

Here is the usage:

public SomeClass {
    private static readonly IDataUpdater<SomeClass, SomeOtherClass> _dataMapper = new DataMapper<SomeClass, SomeOtherClass>()
        .Map(x => x.PropertyA, y => y.PropertyAA)
        .Map(x => x.PropertyB, y => y.PropertyBB, x => Helper.Encrypt(x), y => Helper.Decrypt(y));

    public string PropertyA { get; set; }
    public string PropertyB { get; set; }

    public void LoadFrom(SomeOtherClass source) {
        _dataMapper.Update(source, this);
    }

    public void SaveTo(SomeOtherClass target) {
        _dataMapper.Update(this, target);
    }
}

You can see in class DataHelper in the last overload of method Update that when I want to call the stored converter function, I use helper class InvokeHelper, because I didn't found other way how to call boxed delegate Func. Code for class InvokeHelper is simple - just single static method:

public static class InvokeHelper<TSource, TTarget> {
    public static TTarget Call(Func<TSource, TTarget> converter, TSource source) {
        return converter(source);
    }
}

Is there a way how to do it without reflection? I need to optimalize these transformations for speed.

Thanks.

You can use Delegate.DynamicInvoke to invoke the delegate. Or, use dynamic :

((dynamic)(Delegate)_converters[sourceProperty])(sourceValue);

The (Delegate) cast is not necessary. It's for documentation and runtime assertion purposes. Leave it out if you don't like it.

Actually, you better use delegate instead of object in the dictionary.

If it were me, I would use a little meta-coding with expressions to create a list of compiled and strongly typed delegates. When you call the Update method, you can go through each Action in the list and update the destination from the source.

No reflection and all of the compiling and such is done once, ahead of the Update call.

public class DataMapper<TSourceType, TTargetType> : IDataUpdater<TSourceType, TTargetType>
{
    List<Action<TSourceType, TTargetType>> _mappers = new List<Action<TSourceType, TTargetType>>();
    DataMapper<TTargetType, TSourceType> _reverseMapper;

    public DataMapper() : this(false) { }
    public DataMapper(bool isReverse)
    {
        if (!isReverse)
        {
            _reverseMapper = new DataMapper<TTargetType, TSourceType>(isReverse: true);
        }
    }

    public DataMapper<TSourceType, TTargetType> Map<TSourceValue, TTargetValue>(
        Expression<Func<TSourceType, TSourceValue>> sourcePropExpr,
        Expression<Func<TTargetType, TTargetValue>> targetPropExpr)
    {
        var mapExpression = Expression.Assign(targetPropExpr.Body, sourcePropExpr.Body);

        _mappers.Add(
            Expression.Lambda<Action<TSourceType, TTargetType>>(
                mapExpression,
                sourcePropExpr.Parameters[0],
                targetPropExpr.Parameters[0])
            .Compile());

        if (_reverseMapper != null) _reverseMapper.Map(targetPropExpr, sourcePropExpr);

        return this;
    }

    public DataMapper<TSourceType, TTargetType> Map<TSourceValue, TTargetValue>(
        Expression<Func<TSourceType, TSourceValue>> sourcePropExpr,
        Expression<Func<TTargetType, TTargetValue>> targetPropExpr,
        Func<TSourceValue, TTargetValue> sourceToTargetConverter,
        Func<TTargetValue, TSourceValue> targetToSourceConverter)
    {
        var convertedSourceExpression = Expression.Invoke(Expression.Constant(sourceToTargetConverter), sourcePropExpr.Body);
        var mapExpression = Expression.Assign(targetPropExpr.Body, convertedSourceExpression);

        _mappers.Add(
            Expression.Lambda<Action<TSourceType, TTargetType>>(
                mapExpression,
                sourcePropExpr.Parameters[0],
                targetPropExpr.Parameters[0])
            .Compile());

        if (_reverseMapper != null) _reverseMapper.Map(targetPropExpr, sourcePropExpr, targetToSourceConverter, sourceToTargetConverter);
        return this;
    }

    public void Update(TSourceType source, TTargetType target)
    {
        foreach (var mapper in _mappers)
        {
            mapper(source, target);
        }
    }

    public void Update(TTargetType source, TSourceType target)
    {
        if (_reverseMapper != null)
        {
            _reverseMapper.Update(source, target);
        }
        else
        {
            throw new Exception("Reverse mapper is null.  Did you reverse twice?");
        };
    }
}

The expression is built by taking the expressions that are passed in and using them as parts for the new expression.

Say you called .Map(x => x.PropertyA, y => y.PropertyAA) . You now have 2 expressions each with a parameter x and y and each with a body x.PropertyA and y.PropertyAA .

Now you want to re-assemble these expression parts into an assignment expression like y.PropertyAA = x.PropertyA . This is done in the line var mapExpression = Expression.Assign(targetPropExpr.Body, sourcePropExpr.Body); which gives you an expected expression.

Now when you call Expression.Lambda, you are incorporating the parameters ( x , y ) into a new expression that looks like (x,y) = > y.PropertyAA = x.PropertyA .

Before you can execute this, you need to compile it, hence the .Compile() . But since you only need to compile this once for any given map, you can compile and store the result. The uncompiled expression is of type Expression<Action<TSourceType,TTargetType>> and after it is compiled the resulting type is Action<TSourceType,TTargetType>

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