30

我有一个像这样用 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 返回可查询的 DTO?涵盖了相同的问题(请参阅第二个响应以获得更好的答案),其中建议使用自定义 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();
    }
4

3 回答 3

30

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

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

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

于 2013-01-23T22:12:32.500 回答
30

使用 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。

于 2015-02-25T17:01:48.890 回答
23

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

由于您已经在使用它,Automapper 具有可查询扩展,它允许您仅公开您的 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:我无法在评论中做出体面的回应,所以我在这里添加了它。这里有一篇关于如何做到这一点的帖子,但我今天收到了 403。希望那是暂时的。

基本上,您检查导航属性的 Select 和 Expand 子句。如果它存在,那么你告诉 EF 通过IQueryable<T> 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;
  }
}
于 2015-02-18T20:33:58.800 回答