在另一个Expression<Func>中使用Expression<Func>映射DTO类

6

我正在使用Entity Framework Code First,并尝试从我的实体类映射到我的DTO类。但我很难弄清楚如何编写选择器。

在这个小例子中,我创建了一个Person类和一个Address类。

在DTO类中,我创建了一个选择器,它将我的Entity映射到我的DTO,但是在PersonDto.Selector中不能使用AddressDto.Selector吗?

public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Address Address { get; set; }
    }

    public class Address
    {
        public int Id { get; set; }
        public string Street { get; set; }
    }

现在我正在尝试将其映射到DTO类。

    public class PersonDto
        {
            public static Expression<Func<Person, PersonDto>> Selector = 
            entity => new PersonDto
                {
                     Id = entity.Id,
                     Name = entity.Name,
                     Address = ??? AddressDTO.Selector
                };

            public int Id { get; set; }
            public string Name { get; set; }
            public AddressDto Address { get; set; }
        }

        public class AddressDto
        {
            public static Expression<Func<Address, AddressDto>> Selector = 
            entity => new AddressDto
                 {
                     Id = entity.Id,
                     Street = entity.Street
                 };

            public int Id { get; set; }
            public string Street { get; set; }
        }

我知道我可以将这个内容直接写在PersonDto.Selector里。

Address = new AddressDto
                     {
                         Id = entity.Address.Id,
                         Street = entity.Address.Street
                     };

但我正在寻找一种方法来重用AddressDto类中的选择器。这样可以保持代码整洁,并在类之间分离责任。

2个回答

6

因此,在这里我们需要几个帮助方法,但是一旦我们拥有了它们,事情应该会相当简单。

我们将从这个类开始,它可以替换所有一个表达式的实例为另一个表达式:

internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

接下来是一个扩展方法,使调用更加方便:

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

接下来我们将编写一个compose扩展方法。 它将采用计算中间结果的lambda表达式,然后采用基于中间结果计算最终结果的另一个lambda表达式,并返回一个新的lambda表达式,该lambda表达式将接收初始lambda表达式的返回值并返回最终lambda表达式的输出。实际上,它调用一个函数,然后在第一个函数的结果上调用另一个函数,但使用表达式而不是方法。

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

现在我们将创建一个“Combine”方法。这与之前的方法类似,但略有不同。它将接受一个计算中间结果的lambda表达式和一个使用初始输入和中间输入来计算最终结果的函数。它基本上与“Compose”方法相同,但第二个函数也可以了解第一个参数:
public static Expression<Func<TFirstParam, TResult>>
    Combine<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TFirstParam, TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], param)
        .Replace(second.Parameters[1], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

好的,既然我们已经有了所有这些,接下来我们将使用它。首先,我们将创建一个静态构造函数;我们将不能将所有需要做的事情都内联到字段初始化器中。(另一种选择是创建一个计算此内容的静态方法,并让初始化器调用它。)

之后,我们将创建一个表达式,该表达式接受一个人并返回其地址。这是您拥有的表达式中缺失的拼图中的一个。利用这个,我们将该地址选择器与AddressDto选择器组合起来,然后在其上使用Combine。使用这个,我们有一个 lambda,它接受一个Person和一个AddressDTO,并返回一个PersonDTO。所以在那里,我们基本上有您想要的内容,但是有一个address参数供我们分配给地址:

static PersonDto()
{
    Expression<Func<Person, Address>> addressSelector =
        person => person.Address;

    Selector = addressSelector.Compose(AddressDto.Selector)
            .Combine((entity, address) => new PersonDto
            {
                Id = entity.Id,
                Name = entity.Name,
                Address = address,
            });
}

微软官方文档(尽管是由一个人编写)在此处列出了Expression<Func<>>方法以将您的结果转换为DTO:http://www.asp.net/web-api/overview/web-api-routing-and-actions/create-a-rest-api-with-attribute-routing。我不明白为什么微软没有内置一个制作复杂DTO的方法,而我们不得不使用这些额外的方法(参见“hack”)来完成。肯定这不是一个独特的用户案例。 - nzondlo
@nzondlo 嗯,任何“自动”的解决方案可能都需要进行黑客攻击。它需要猜测并判断它认为正在发生的事情。通过手动操作,您可以确保它是正确的,因此它不太像黑客攻击。一旦您建立了一些基本工具来操作表达式,这也并不难。 (其中一些我确实希望能够在基础类库中。我几乎从不回答与“Expression”相关的问题,而不附加自己的“Replace”定义。)这些辅助方法是有意编写成可重用的。 - Servy
也许我更多地考虑了你对我的问题给出的答案,这个问题非常相似。我不明白为什么这种功能不能集成到基于方法的 Linq-To-Entity API 中。 - nzondlo

-4

首先,我会只使用一个类来完成这个任务。不需要与其相同的DTO类。

如果您坚持要有第二个DTO类,那么我会简单地为它们创建扩展方法。例如:PersonDTO personDto = myPerson.ToDTO()


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接