3

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

域实体:

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

问题

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

如何才能做到这一点?

4

2 回答 2

9

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

请参阅 wiki 上的投影页面,了解您所追求的东西。

更新当您使用实体框架时,这里有一个更新,用于将您的表达式从 using 重新映射AccountModelAccount

在您的应用程序的CompositionRoot中,像这样设置 AutoMapper(如果您不使用 Code Contracts,请忽略 Code Contract 语句):

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>();
于 2012-06-28T16:13:07.177 回答
2

您可以使用 anExpressionVisitor来重写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;
    }

}

此访问者将输入参数从 type 切换AccountModelAccount(即VisitLambdaandVisitParameter方法),并更改所有属性访问器以使用此新参数,并在适当时切换属性名称(这是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);
于 2012-06-28T16:07:08.477 回答