C#表达式树:将实体参数转换为接口

时间:2017-02-18 18:58:11

标签: c# lambda expression-trees

我正在尝试构建一个流体API,用于通过表达式树在对象上设置属性值。而不是这样做:

public static class Converters
{
    public static SomeType ToSomeType( this Dictionary<string, string> values, string fieldName )
    {
        //...conversion logic
    }
}

public class Target : ITarget
{
    public SomeType Prop1 {get; set;}

    public void SetValues( Dictionary<string, string> values )
    {
        Prop1 = values.ToSomeType("fieldName");
    }
}

我希望能够这样做:

public class Target : ITarget
{
    public Target()
    {
        this.SetProperty( x=>x.Prop1, y => y.ToSomeType("fieldName") );
    }

    public void SetValues( Dictionary<string, string> values )
    {
        //...logic that executes compiled converter functions derived from
        // SetProperty calls, and which are stored in an internal list
    }
}

我在SetProperty静态方法上取得了一些进展,让我遇到了一个问题,我需要将同一个对象作为特定类的实例引用(在我的示例中为Target) )作为一个ITarget:

public static void SetProperty<TEntity, TProperty>( this TEntity target, Expression<Func<TEntity, object>> memberLambda,
        Expression<Func<IImportFile, TProperty>> converter )
        where TEntity: class, ITarget
{
    var memberSelector = memberLambda.Body as MemberExpression;
    if( memberSelector == null )
        throw new ArgumentException(
                $"{nameof( SetProperty )} -- invalid property specification on Type {typeof(TEntity).FullName}" );

    var propInfo = memberSelector.Member as PropertyInfo;

    if( propInfo == null )
        throw new ArgumentException(
                $"{nameof( SetProperty )} -- invalid property specification on Type {typeof( TEntity ).FullName}" );

    MethodCallExpression convMethod = converter.Body as MethodCallExpression;
    if( convMethod == null )
        throw new ArgumentException(
                $"{nameof( SetProperty )} -- converter does not contain a MethodCallExpression on Type {typeof( IImportFile ).FullName}" );

    ParameterExpression targetExp = Expression.Parameter( typeof(TEntity), "target" );
    MemberExpression propExp = Expression.Property( targetExp, propInfo );

    BinaryExpression assignExp = Expression.Assign( propExp, convMethod );

    // this next line throws the exception
    var junk = Expression.Lambda<Action<ITarget, IImportFile>>( assignExp, targetExp,
                (ParameterExpression) convMethod.Arguments[ 0 ] ).Compile();
}

问题发生在SetProperty实现的最后一行。编译器不会接受第二个参数 - 从convMethod的参数派生的参数 - 因为,就其而言,TEntity!= ITarget。

当然,除了我的例子中的TEntity - Target - 被定义为实现ITarget :)。

我认为Expression编译代码正在进行相当严格的类型检查,而不是查看参数是否代表可以投射到所需内容的东西。

但我无法弄清楚如何将ParameterExpression转换为不同的Type,同时仍然让它引用相同的参数。我尝试过Expression.Convert(),但这不起作用,因为它返回一个UnaryExpression,Expression.Lambda调用它不会作为ParameterExpression。

跟进#1

我将对IImportTarget的引用更正为ITarget。对此感到抱歉。

我没有解释整个系统,因为它相当大,而且具体问题 - 你如何让两个ParameterExpressions引用同一个对象,但是它们是不同的类型(它们通过公共接口相关联 - )应该会在很多地方出现。

以下是确切的异常消息:

  

发生System.ArgumentException HResult = -2147024809
  Message = ParameterExpression类型&#39; ConsoleApp1.TestTarget&#39;不可能是   用于类型&#39; ImportFramework.IImportTarget&#39;
的委托参数   Source = System.Core StackTrace:          在System.Linq.Expressions.Expression.ValidateLambdaArgs(类型delegateType,Expression&amp; body,ReadOnlyCollection 1 parameters) at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, String name, Boolean tailCall, IEnumerable 1个参数)          在System.Linq.Expressions.Expression.Lambda [TDelegate](表达体,   布尔值tailCall,IEnumerable 1 parameters) at ImportFramework.ImportAgentExtensions.SetProperty[TEntity,TProperty](TEntity target, Expression 1 memberLambda,Expression`1转换器)in   C:\程序\ ConnellCampaigns的\ src \ ImportFramework \ ImportAgent.cs:行   55 InnerException:

1 个答案:

答案 0 :(得分:0)

该解决方案,或至少&gt;&gt;&lt;&lt;&lt;&lt;解决方案:),是要更改将异常抛出到以下内容的最后一行:

var junk = Expression.Lambda<Action<TEntity, IImportFile>>( assignExp, targetExp, (ParameterExpression) convMethod.Arguments[ 0 ] ).Compile();

然后还更改设置值的方法以使用扩展方法:

public static void ImportValues<TEntity>( this TEntity target, IImportFile importer )
    where TEntity : class, IImportTarget
{
    foreach( Action<TEntity, IImportFile> setter in ( ( IImportTargetSetValues ) target ).Setters )
    {
        setter( target, importer );
    }
}

这允许我在编译方法和使用它时指定实体Type(TEntity)。在ImportValues()方法中循环中的强制转换是必要的,因为我将setter作为普通旧对象存储在与TEntity实例关联的列表中。

这可能会有问题,因为人们永远不会知道对象列表中的内容。 OTOH,该列表仅通过内部接口提供,并且我使用LinkProperty扩展方法控制添加到其中的内容,因此在实践中它不是问题。