使用“基本”自定义方法将EntityModel转换为DTO

时间:2019-03-18 12:44:43

标签: c# entity-framework linq

首先,我很抱歉这是一个骗子,找到正确的搜索词似乎是不可能的...

我们正在尝试采用一些最佳实践,并希望在我们的项目中重构重复的代码。在很多情况下,我们都有类似的东西

public List<EventModel> GetEvents(bool showInactive, bool showPastEvents)
{
    return eventRepository
        .GetEvents(_customerId, showInactive, showPastEvents)
        .Select(e => New EventModel() {  Id = e.EventId, Name = e.EventName, Capacity = e.EventCapacity, Active = e.EventActive })
        .ToList();
}

所以我们尝试做这样的事情;

public List<EventModel> GetEvents(bool showInactive, bool showPastEvents)
{
    return eventRepository
        .GetEvents(_customerId, showInactive, showPastEvents)
        .Select(e => ConvertPocoToModel(e))
        .ToList();
}

private EventModel ConvertPocoToModel(TsrEvent tsrEvent)
{
    EventModel eventModel = new EventModel()
    {
        Id = tsrEvent.EventId,
        Name = tsrEvent.EventName,
        Capacity = tsrEvent.EventCapacity,
        Active = tsrEvent.EventActive                
    };
    return eventModel;
}

有时候这行得通,但是间歇性地我们得到了;

  

System.NotSupportedException:'LINQ to Entities无法识别   方法'Bll.Models.EventModel ConvertPocoToModel(Dal.Pocos.TsrEvent)'   方法,并且该方法无法转换为商店表达式。'

我知道我们可以添加.ToList()或类似的代码来强制在C#中进行转换,但是我认为这意味着SQL将执行SELECT *而不是SELECT EVentId, EventName, EventCapacity, EventActive

任何人都可以解释;

  • 为什么EF在尝试理解如何处理这种简单映射时遇到问题?
  • 为什么它会间歇性地工作?
  • 我们应该怎么做?

2 个答案:

答案 0 :(得分:2)

实体框架不知道如何转换您的方法。您必须使用返回Expression<Func<TsrEvent,EventModel>>的方法或存储它的属性。

public List<EventModel> GetEvents(bool showInactive, bool showPastEvents)
{
    return eventRepository
        .GetEvents(_customerId, showInactive, showPastEvents)
        .Select(ConvertPocoToModelExpr)
        .ToList();
}

private static Expression<Func<TsrEvent,EventModel>> ConvertPocoToModelExpr =>  (x)=>new EventModel()
    {
        Id = x.EventId,
        Name = x.EventName,
        Capacity = x.EventCapacity,
        Active = x.EventActive                
    };

答案 1 :(得分:1)

您必须了解IEnumerableIQueryable之间的区别。

IEnumerable对象包含要枚举序列的所有内容。您可以要求第一个元素,一旦有了一个元素,就可以要求下一个,只要有下一个。 IEnumerable被您的进程本地化。

通过请求Enumerator并反复调用MoveNext来进行最低级别的枚举,直到不再需要任何元素为止。像这样:

IEnumerable<Student> students = ...
IEnumerator<Student> studentEnumerator = students.GetEnumerator();
while (studentEnumerator.MoveNext())
{
    // there is still a Student to process:
    Student student = studentEnumerator.Current;
    ProcessStudent(student);
}

您可以显式执行此操作,也可以使用foreach或LINQ函数之一隐式调用它。

另一方面,IQueryable是由不同的进程(通常是数据库管理系统)处理的。 IQueryable包含一个Expression和一个ProviderExpression表示必须以某种通用格式执行的查询。 Provider知道谁必须执行查询(通常是数据库管理系统),以及该过程使用的语言(通常是类似SQL的语言)。

通过调用GetEnumerator进行枚举后,Expression将立即发送到Provider,后者会尝试将Expression转换为SQL并执行查询。提取的数据被放入一个可枚举的序列中,并返回枚举数。

返回您的问题

问题是,SQL不知道ConvertPocoToModel。因此,您的提供程序无法转换Expression。编译器无法检测到这一点,因为它不知道您的Provider有多聪明。这就是为什么直到您致电GetEnumerator(在您的情况下,致电ToList)您都不会收到此错误的原因。

解决方案

解决方案是创建一个更改表达式的函数。最简单的方法是扩展功能。参见extension methods demystified。这样,您可以像使用其他任何LINQ方法一样使用它:

public static IQueryable<EventModel> ToEventModels(this IQueryable<TsrEvent> tsrEvents)
{
    return tsrEvent.Select(tsrEvent =>  new EventModel
    {
        Id = tsrEvent.EventId,
        Name = tsrEvent.EventName,
        Capacity = tsrEvent.EventCapacity,
        Active = tsrEvent.EventActive                
    };
}

请注意,我在构造函数中省略了():SQL无法调用构造函数!

用法:

var result = dbContext.TsrEvents
     .Where(tsrEvent => tsrEvent.Active && tsrEvent.Date == Today)
     .ToEventModels()
     .GroupBy(...)
     ... etc

或者,如果您的GetEvents返回IQueryable<TsrEvents>

return eventRepository.GetEvents(_customerId, showInactive, showPastEvents)
      .ToEventModels();

最终备注

最好让您的数据获取功能尽可能长地返回IQueryable<...>IEnumerable<...>。仅让最终用户实现查询。如果您执行ToList()而呼叫者只想执行FirstOrDefault()

,那将浪费处理能力。