如何映射两种不同类型的表达式?

时间:2012-06-28 16:04:58

标签: c# linq

我将从一些课程开始......

域名实体:

public class Account
{
    public int Id { get; set; }
    public double Balance { get; set; }
    public string CustomerName { get; set; }
}

视图模型:

public class AccountModel
{
    public int Id { get; set; }
    public double Bal { get; set; }
    public string Name { get; set; }
}

存储库:

我的存储库上有一个方法,它接受一个表达式并返回一个列表,如下所示:

public interface IAccountRepository
{
    IEnumerable<Account> Query(Expression<Func<Account, bool>> expression);
} 

问题

我的应用在UI中生成Expression<Func<AccountModel, bool>>。我需要以某种方式将 EXPRESSION AccountModel转换或映射到Account,以便我可以在Query方法中使用它。我说“map”是因为,如果你注意到,我的模型和域对象是相似的,但不一定具有相同的属性名称。

如何做到这一点?

2 个答案:

答案 0 :(得分:9)

这听起来像是AutoMapper的工作。 Automapper允许您在一个时间点将一个类映射到另一个类,并在以后使用此映射配置。

请参阅维基上的Projection页面了解您所追求的内容。

更新当您使用实体框架时,此处有一个更新,用于将您的表达式从使用AccountModel重新映射到Account

在您的应用程序的CompositionRoot中,像这样设置AutoMapper(如果您不使用代码合同,则忽略代码合同声明):

var accountModelMap = Mapper.CreateMap<AccountModel, Account>();

Contract.Assume(accountModelMap != null);
accountModelMap.ForMember(account => account.Id, expression => expression.MapFrom(model => model.Id));
accountModelMap.ForMember(account => account.Balance, expression => expression.MapFrom(model => model.Bal));
accountModelMap.ForMember(account => account.CustomerName, expression => expression.MapFrom(model => model.Name));

这将配置两种数据类型如何相互关联。

实施ExpressionVisitor以使用AutoMapper将成员访问权限重新绑定到另一种类型。

/// <summary>
/// An <see cref="ExpressionVisitor"/> implementation which uses <see href="http://automapper.org">AutoMapper</see> to remap property access from elements of type <typeparamref name="TSource"/> to elements of type <typeparamref name="TDestination"/>.
/// </summary>
/// <typeparam name="TSource">The type of the source element.</typeparam>
/// <typeparam name="TDestination">The type of the destination element.</typeparam>
public class AutoMapVisitor<TSource, TDestination> : ExpressionVisitor
{
    private readonly ParameterExpression _newParameter;
    private readonly TypeMap _typeMap = Mapper.FindTypeMapFor<TSource, TDestination>();

    /// <summary>
    /// Initialises a new instance of the <see cref="AutoMapVisitor{TSource, TDestination}"/> class.
    /// </summary>
    /// <param name="newParameter">The new <see cref="ParameterExpression"/> to access.</param>
    public AutoMapVisitor(ParameterExpression newParameter)
    {
        Contract.Requires(newParameter != null);

        _newParameter = newParameter;
        Contract.Assume(_typeMap != null);
    }

    [ContractInvariantMethod]
    [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
    private void ObjectInvariant()
    {
        Contract.Invariant(_typeMap != null);
        Contract.Invariant(_newParameter != null);
    }

    /// <summary>
    /// Visits the children of the <see cref="T:System.Linq.Expressions.MemberExpression"/>.
    /// </summary>
    /// <returns>
    /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
    /// </returns>
    /// <param name="node">The expression to visit.</param>
    protected override Expression VisitMember(MemberExpression node)
    {
        var propertyMaps = _typeMap.GetPropertyMaps();
        Contract.Assume(propertyMaps != null);

        // Find any mapping for this member
        var propertyMap = propertyMaps.SingleOrDefault(map => map.SourceMember == node.Member);
        if (propertyMap == null)
            return base.VisitMember(node);

        var destinationProperty = propertyMap.DestinationProperty;

        Contract.Assume(destinationProperty != null);
        var destinationMember = destinationProperty.MemberInfo;

        Contract.Assume(destinationMember != null);

        // Check the new member is a property too
        var property = destinationMember as PropertyInfo;
        if (property == null)
            return base.VisitMember(node);

        // Access the new property
        var newPropertyAccess = Expression.Property(_newParameter, property);
        return base.VisitMember(newPropertyAccess);
    }
}

然后实现一个扩展方法,使其更易于使用:

/// <summary>
/// A class which contains extension methods for <see cref="Expression"/> and <see cref="Expression{TDelegate}"/> instances.
/// </summary>
public static class ExpressionExtensions
{
    /// <summary>
    /// Remaps all property access from type <typeparamref name="TSource"/> to <typeparamref name="TDestination"/> in <paramref name="expression"/>.
    /// </summary>
    /// <typeparam name="TSource">The type of the source element.</typeparam>
    /// <typeparam name="TDestination">The type of the destination element.</typeparam>
    /// <typeparam name="TResult">The type of the result from the lambda expression.</typeparam>
    /// <param name="expression">The <see cref="Expression{TDelegate}"/> to remap the property access in.</param>
    /// <returns>An <see cref="Expression{TDelegate}"/> equivalent to <paramref name="expression"/>, but applying to elements of type <typeparamref name="TDestination"/> instead of <typeparamref name="TSource"/>.</returns>
    public static Expression<Func<TDestination, TResult>> RemapForType<TSource, TDestination, TResult>(this Expression<Func<TSource, TResult>> expression)
    {
        Contract.Requires(expression != null);
        Contract.Ensures(Contract.Result<Expression<Func<TDestination, TResult>>>() != null);

        var newParameter = Expression.Parameter(typeof (TDestination));

        Contract.Assume(newParameter != null);
        var visitor = new AutoMapVisitor<TSource, TDestination>(newParameter);
        var remappedBody = visitor.Visit(expression.Body);
        if (remappedBody == null)
            throw new InvalidOperationException("Unable to remap expression");

        return Expression.Lambda<Func<TDestination, TResult>>(remappedBody, newParameter);
    }
}

随后可以这样使用(在NUnit测试中):

[TestFixture]
public class RemappingTests
{
    #region Setup/Teardown
    /// <summary>
    /// Sets up the variables before each test.
    /// </summary>
    [SetUp]
    public void Setup()
    {
        var accountModelMap = Mapper.CreateMap<AccountModel, Account>();
        Contract.Assume(accountModelMap != null);
        accountModelMap.ForMember(account => account.Id, expression => expression.MapFrom(model => model.Id));
        accountModelMap.ForMember(account => account.Balance, expression => expression.MapFrom(model => model.Bal));
        accountModelMap.ForMember(account => account.CustomerName, expression => expression.MapFrom(model => model.Name));
    }

    [TearDown]
    public void Teardown()
    {
        Mapper.Reset();
    }
    #endregion

    /// <summary>
    /// Checks that <see cref="ExpressionExtensions.RemapForType{TSource, TDestination, TResult}(Expression{Func{TSource, TResult}})"/> correctly remaps all property access for the new type.
    /// </summary>
    /// <param name="balance">The balance to use as the value for <see cref="Account.Balance"/>.</param>
    /// <returns>Whether the <see cref="Account.Balance"/> was greater than 50.</returns>
    [TestCase(0, Result = false)]
    [TestCase(80, Result = true)]
    public bool RemapperUsesPropertiesOfNewDataType(double balance)
    {
        Expression<Func<AccountModel, bool>> modelExpr = model => model.Bal > 50;

        var accountExpr = modelExpr.RemapForType<AccountModel, Account, bool>();

        var compiled = accountExpr.Compile();
        Contract.Assume(compiled != null);

        var hasBalance = compiled(new Account {Balance = balance});

        return hasBalance;
    }
}

如果代码太多而无法找到确切的调用,那么它就是:

Expression<Func<AccountModel, bool>> modelExpr = model => model.Bal > 50;
var accountExpr = modelExpr.RemapForType<AccountModel, Account, bool>();

答案 1 :(得分:2)

您可以使用ExpressionVisitor重写Expression

public class AccountModelRewriter : ExpressionVisitor
{

    private Stack<ParameterExpression[]> _LambdaStack = new Stack<ParameterExpression[]>();

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        var lambda = (LambdaExpression)node;

        _LambdaStack.Push(
            lambda.Parameters.Select(parameter => typeof(AccountModel) == parameter.Type ? Expression.Parameter(typeof(Account)) : parameter)
            .ToArray()
        );

        lambda = Expression.Lambda(
            this.Visit(lambda.Body),
            _LambdaStack.Pop()
        );

        return lambda;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        var memberExpression = (MemberExpression)node;

        var declaringType = memberExpression.Member.DeclaringType;
        var propertyName = memberExpression.Member.Name;

        if (typeof(AccountModel) == declaringType)
        {
            switch (propertyName)
            {
                case "Bal" :
                    propertyName = "Balance";
                    break;
                case "Name" :
                    propertyName = "CustomerName";
                    break;
            }

            memberExpression = Expression.Property(
                this.Visit(memberExpression.Expression),
                typeof(Account).GetProperty(propertyName)
            );
        }

        return memberExpression;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        node = (ParameterExpression)base.VisitParameter(node);
        if (typeof(AccountModel) == node.Type)
        {
            node = this._LambdaStack.Peek().Single(parameter => parameter.Type == typeof(Account));
        }
        return node;
    }

}

此访问者将输入参数从AccountModel类型切换为AccountVisitLambdaVisitParameter方法),并将所有属性访问者更改为使用这个新参数以及在适当的情况下切换属性名称(VisitMember部分)。

用法如下:

Expression<Func<AccountModel, bool>> accountModelQuery = a => a.Bal == 0 && a.Name != null && a.Id != 7;

var accountQuery = (Expression<Func<Account, bool>>)new AccountModelRewriter().Visit(accountModelQuery);