Reputation:
I'm trying to build a fluid API for setting property values on an object via expression trees. Rather than do this:
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");
}
}
I'd like to be able to do this:
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
}
}
I've made some progress on the SetProperty static method, put I'm running into a problem where I need to refer to the same object as both an instance of a particular class (Target, in my example) and as an 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();
}
The problem occurs on the very last line of the SetProperty implementation. The compiler won't accept the second parameter -- the one derived from the arguments of convMethod -- because, as far as it's concerned, TEntity != ITarget.
Except, of course, that TEntity -- Target in my example -- is defined to implement ITarget :).
I presume the Expression compiling code is doing what amounts to really strict Type checking, and is not looking at whether a parameter represents something that could be cast into what is needed.
But I can't figure out how to cast a ParameterExpression to a different Type, while still having it refer to the same parameter. I tried Expression.Convert(), but that doesn't work because it returns a UnaryExpression, which the Expression.Lambda call won't take as a ParameterExpression.
Follow Up #1
I corrected the reference to IImportTarget, to be ITarget. Sorry about the confusion.
I didn't explain the entire system that this is part of because it's fairly large, and the specific question -- how do you have two ParameterExpressions refer to the same object, but be different Types (which are related through a public interface) -- is something that should crop up in lots of places.
Here is the exact exception message:
System.ArgumentException occurred HResult=-2147024809
Message=ParameterExpression of type 'ConsoleApp1.TestTarget' cannot be used for delegate parameter of type 'ImportFramework.IImportTarget'
Source=System.Core StackTrace: at System.Linq.Expressions.Expression.ValidateLambdaArgs(Type delegateType, Expression& body, ReadOnlyCollection1 parameters) at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, String name, Boolean tailCall, IEnumerable
1 parameters) at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, Boolean tailCall, IEnumerable1 parameters) at ImportFramework.ImportAgentExtensions.SetProperty[TEntity,TProperty](TEntity target, Expression
1 memberLambda, Expression`1 converter) in C:\Programming\ConnellCampaigns\src\ImportFramework\ImportAgent.cs:line 55 InnerException:
Upvotes: 1
Views: 869
Reputation:
The solution, or at least >>a<< solution :), was to change that last line which threw the exception to the following:
var junk = Expression.Lambda<Action<TEntity, IImportFile>>( assignExp, targetExp, (ParameterExpression) convMethod.Arguments[ 0 ] ).Compile();
and then also change the approach to setting values to use an extension method:
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 );
}
}
This allowed me to specify the entity Type (TEntity) both when the method was being compiled and when it was being used. The cast in the loop within the ImportValues() method is necessary because I stored the setters as plain old objects in a list associated with the TEntity instance.
That could be problematic, since one never knows what's in a List of objects. OTOH, the list is only available via an internal interface, and I control what gets added to it with the LinkProperty extension method, so it's not a problem in practice.
Upvotes: 0