【问题标题】:Web API Queryable - how to apply AutoMapper?Web API Queryable - 如何应用 AutoMapper?
【发布时间】:2013-01-07 05:25:37
【问题描述】:

我有一个像这样用 OData 可查询属性装饰的简单 WebApi 方法。

    [Queryable]
    public virtual IQueryable<PersonDto> Get()
    {
        return uow.Person().GetAll()); // Currently returns Person instead of PersonD
    }

我想要做的是在 WebAPI 将结果转换为 JSON 之前,使用 AutoMapper 将查询结果从 Person 类型转换为 PersonDto 类型。

有人知道我该怎么做吗?我知道,我可以在 GetAll() 调用之后应用 Mapper.Map,然后再转换回 IQueryable,但这会导致在应用 OData 过滤器之前返回并映射整个表(不好!)。

看来这个问题ASP.NET Web API return queryable DTOs? 涵盖了相同的问题(请参阅第二个回复以获得更好的答案),其中建议使用自定义 MediaTypeFormatter 在链的末尾使用 AutoMapper,但是我不知道如何根据我看到的例子来做。

任何帮助将不胜感激!

-- 更多信息

我查看了 IQueryable 的源代码,但不幸的是,我看不到任何将代码用于此目的的方法。我已经设法编写了一个看起来可以工作的附加过滤器,但它肯定不是优雅的。

public class PersonToPersonDtoConvertAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(System.Web.Http.Filters.HttpActionExecutedContext actionExecutedContext)
    {
        HttpResponseMessage response = actionExecutedContext.Response;

        if (response != null)
        {
            ObjectContent responseContent = response.Content as ObjectContent;
            var query = (responseContent.Value as IQueryable<Student>).ToList();
            response.Content = new ObjectContent<IEnumerable<StudentResource>>(query.ToList().Select(Mapper.Map<Person, PersonDto>), responseContent.Formatter);
        }
    }
}

然后我将动作修饰为

    [Queryable]
    [PersonToPersonDtoConvert]
    public IQueryable<Person> Get()
    {
        return uow.GetRepo<IRepository<Person>>().GetAll();
    }

【问题讨论】:

  • 我已经更新了一个似乎可行的示例,但我确信一定有更好的解决方案。

标签: asp.net .net asp.net-mvc asp.net-web-api


【解决方案1】:

有一个更好的解决方案。试试这个:

public virtual IQueryable<PersonDto> Get(ODataQueryOptions<Person> query)
{
    var people = query.ApplyTo(uow.Person().GetAll());
    return ConvertToDtos(people);
}

这将确保查询在 Person 而不是 PersonDTO 上运行。如果您希望通过属性而不是在代码中进行转换,您仍然需要实现一个类似于您提出的操作过滤器。

【讨论】:

  • 天哪,我不能给你足够多的支持!感谢您为我找到那个!对于其他人,您可以在 MS 文档中找到此示例 adding OData to your WebAPI 大约在页面的中间位置。您使用ODataQueryOptions&lt;T&gt; 而不是 [Queryable]
  • 什么都试过了,我总是得到 Not Acceptable 406 状态码。返回 Dto 时:(
  • 您需要在模型构建器上创建这两个对象才能工作。否则将导致 406 Not Acceptable。示例:builder.EntitySet&lt;Person&gt;("Persons");builder.EntitySet&lt;PersonDto&gt;("PersonsDto");
  • 这不是在自动映射器中使用 DTO 的方式。看看 alik 的答案,了解正确的做法。此解决方案可能有效,但您并未在此处充分利用 Automapper。即使您不使用 Automapper,我仍然认为这不是要走的路。
  • 我同意。下面的解决方案更好。如果可以的话,您应该将查询应用到您的 DTO。
【解决方案2】:

使用 AutoMapper 的 Queryable Extensions

首先,定义映射。

// Old AutoMapper API
// Mapper.CreateMap<Person, PersonDto>();

// Current AutoMapper API
Mapper.Initialize(cfg => 
   cfg.CreateMap<Person, PersonDto>()
);

然后你可以使用这样的东西:

[EnableQuery]
public IQueryable<PersonDto> Get() {
    // Old AutoMapper API
    // return this.dbContext.Persons.Project().To<PersonDto>();

    // New AutoMapper API
    return this.dbContext.Persons.ProjectTo<PersonDto>();
}

编辑 04/2019:更新以反映当前的 AutoMapper API。

【讨论】:

  • !!!这应该是公认的答案!!!这将像您对使用 EF 的常规 ODATA 查询所期望的那样,将过滤器从 ODATA 应用到 SQL 查询。您不需要 WebApi 中的任何其他配置。
  • 等等!这还不够!虽然IQueryable&lt;T&gt; 会有所帮助,但如果调用者传入$filter$select$expand 之类的查询选项,那么所有人员将首先加载到内存中,然后然后查询选项将被应用,而不是你需要UseAsDataSource
【解决方案3】:

恕我直言,接受的解决方案不正确。一般来说,如果您的服务使用 DTO,您不希望将底层实体(人员)暴露给服务。为什么要查询 Person 模型并返回 PersonDTO 对象?

由于您已经在使用它,Automapper 具有Queryable Extensions,它允许您仅公开您的 DTO 并将过滤应用于数据源的基础类型。例如:

public IQueryable<PersonDto> Get(ODataQueryOptions<PersonDto> options) {
    Mapper.CreateMap<Person, PersonDto>();
    var persons = _personRepository.GetPersonsAsQueryable();
    var personsDTOs = persons.Project().To<PersonDto>();  // magic happens here...

    return options.ApplyTo(personsDTOs);
}

关于急切加载导航属性...

@philreed:我无法在评论中做出体面的回应,所以我在这里添加了它。有一篇关于如何做到这一点的帖子here 但我今天得到了 403。希望那是暂时的。

基本上,您检查导航属性的 Select 和 Expand 子句。如果它存在,那么你告诉 EF 通过IQueryable&lt;T&gt; Include 扩展方法热切加载。

控制器

public IQueryable<MyDto> GetMyDtos(ODataQueryOptions<MyDto> options)
{   
  var eagerlyLoad = options.IsNavigationPropertyExpected(t => t.MyNavProperty);
  var queryable = _myDtoService.GetMyDtos(eagerlyLoad);

  // _myDtoService will eagerly load to prevent select N+1 problems
  // return (eagerlyLoad) ? efResults.Include(t => t.MyNavProperty) : efResults;

  return queryable;
}

扩展方法

public static class ODataQueryOptionsExtensions
{
  public static bool IsNavigationPropertyExpected<TSource, TKey>(this ODataQueryOptions<TSource> source, Expression<Func<TSource, TKey>> keySelector)
  {
    if (source == null) { throw new ArgumentNullException("source"); }
    if (keySelector == null) { throw new ArgumentNullException("keySelector"); }

    var returnValue = false;
    var propertyName = (keySelector.Body as MemberExpression ?? ((UnaryExpression)keySelector.Body).Operand as MemberExpression).Member.Name;
    var expandProperties = source.SelectExpand == null || string.IsNullOrWhiteSpace(source.SelectExpand.RawExpand) ? new List<string>().ToArray() : source.SelectExpand.RawExpand.Split(',');
    var selectProperties = source.SelectExpand == null || string.IsNullOrWhiteSpace(source.SelectExpand.RawSelect) ? new List<string>().ToArray() : source.SelectExpand.RawSelect.Split(',');

    returnValue = returnValue ^ expandProperties.Contains<string>(propertyName);
    returnValue = returnValue ^ selectProperties.Contains<string>(propertyName);

    return returnValue;
  }
}

【讨论】:

  • 为什么我在尝试此操作时会得到:“无法将其解析为可查询表达式”?
  • 你说得对,本。 Automapper 的可查询扩展非常糟糕。接受的答案是不正确的,甚至被称为坏的。你的答案接近正确。但我认为阿利克的答案更接近。不需要 ODataQueryOptions 对象,它会在使用 alik 指定的属性时发生。
  • @FrederikPrijck,你是对的。在这种情况下,它们产生相同的功能。如果我需要检查传入的查询并根据查询选项执行其他操作,我会使用 ODataQueryOptions。例如。强制 EF 预先加载导航属性以提高 SQL 性能。
  • @philreed 我更新了我的答案 b/c 我无法在评论中做出体面的回应。希望对您有所帮助。
  • @philreed 我可能不会。你的用例看起来比我的更复杂。我只需要深入一次expand。祝你好运。 ;)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多