如何将针对 DTO 的 OData 查询映射到另一个实体?

Posted

技术标签:

【中文标题】如何将针对 DTO 的 OData 查询映射到另一个实体?【英文标题】:How do I map an OData query against a DTO to another entity? 【发布时间】:2014-12-25 01:11:05 【问题描述】:

我的问题和这个问题很相似:How do I map an OData query against a DTO to an EF entity? 我有一个简单的设置来测试 ASP.NET Web API OData V4 $filter 功能。我想做的是给 ProductDTO 的一些属性“别名”以匹配 Product 实体的属性。例如,用户将通过以下请求调用 ProductsController:

GET products?$filter=DisplayName eq ‘test’

产品类:

public class Product

    public int Id  get; set; 
    public string Name  get; set; 
    public int Level  get; set; 
    public Product()
     

ProductDTO 类:

public class ProductDTO

    public int Id  get; set; 
    public string DisplayName  get; set; 
    public int DisplayLevel  get; set; 
    public ProductDTO(Product product)
    
        this.DisplayName = product.Name;
        this.DisplayLevel = product.Level;
    

产品控制器:

public class ProductsController : ApiController

    public IEnumerable<ProductDTO> Get(ODataQueryOptions<Product> q)
    
        IQueryable<Product> products = this._products.AsQueryable();
        if (q.Filter != null) products = q.Filter.ApplyTo(this._products.AsQueryable(), new ODataQuerySettings()) as IQueryable<Product>;
        return products.Select(p => new ProductDTO(p));
    

当然,我遇到了以下异常:

在“TestAPI.Models.Product”类型上找不到名为“DisplayName”的属性

我尝试通过将以下行添加到 WebApiConfig.cs 来使用新引入的别名功能

public static class WebApiConfig

    public static void Register(HttpConfiguration config)
    
        …
        IEdmModel model = GetModel();
        config.MapODataServiceRoute("*", "*", model);
    

    private static IEdmModel GetModel()
    
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        EntitySetConfiguration<Product> products = builder.EntitySet<Product>("Product");
        products.EntityType.Property(p => p.Name).Name = "DisplayName";
        products.EntityType.Property(p => p.Level).Name = "DisplayLevel";
        return builder.GetEdmModel();
    

我想我错误地使用了别名功能,因为引发了与上述相同的异常。如果我调用以下请求,它会起作用,但这不是我想要实现的目标:

GET products?$filter=Name eq ‘test’

更新:

我同意 gdoron 的观点,Get 端点应该是这样的:

public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)

但这应该可以在没有 AutoMapper 的情况下解决吗?

【问题讨论】:

【参考方案1】:

我找到了一个不使用 AutoMapper 的解决方案。

ProductsController 现在看起来像这样:

public class ProductsController : ApiController

    public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)
    
        IQueryable<Product> products = this._products.AsQueryable();

        IEdmModel model = GetModel();
        IEdmType type = model.FindDeclaredType("TestAPI.Models.Product");
        IEdmNavigationSource source = model.FindDeclaredEntitySet("Products");
        ODataQueryOptionParser parser = new ODataQueryOptionParser(model, type, source, new Dictionary<string, string>   "$filter", q.Filter.RawValue  );
        ODataQueryContext context = new ODataQueryContext(model, typeof(Product), q.Context.Path);
        FilterQueryOption filter = new FilterQueryOption(q.Filter.RawValue, context, parser);

        if (filter != null) products = filter.ApplyTo(products, new ODataQuerySettings()) as IQueryable<Product>;
        return products.Select(p => new ProductDTO(p));
    

WebApiConfig:

public static class WebApiConfig

    public static void Register(HttpConfiguration config)
    
        …
        IEdmModel model = GetModel();
        config.MapODataServiceRoute("*", "*", model);
    

    private static IEdmModel GetModel()
    
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        EntitySetConfiguration<Product> product = builder.EntitySet<Product>("Products");
        product.EntityType.Name = "Product";
        product.EntityType.Namespace = "TestAPI.Models";
        product.EntityType.Property(p => p.Name).Name = "DisplayName";
        product.EntityType.Property(p => p.Level).Name = "DisplayLevel";
        return builder.GetEdmModel();
    

【讨论】:

聪明!为什么使用自动映射器,而你可以用 C# 完全做到这一点。也可以使用可查询和表达式树来完成。 ODataQueryOptions 仅支持 $filter, $orderby, $top, $skip 。 如果有人需要new ODataQueryContext(model, typeof(TypeName), q.Context.Path)也可以new ODataQueryContext(model, typeof(TypeName), null) 嗨@niklr,你能回答这个问题吗?谢谢,***.com/questions/62650392/…【参考方案2】:

如果您决定要使用 DTO(我认为这绝对是个好主意),那么就使用它...$metadata 应该反映 DTO 的属性名称,而不是 EF 实体的名称,因为这是客户端得到的,也是客户端应该发送的。 这意味着您应该将Get 端点更改为如下内容:

public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)

为了避免ProductDTOProduct 之间的耦合,您可以使用AutoMapper 为您在类之间进行映射。此外,如果您使用 AutoMapper 的 Project 方法,您可以将您的方法清理为类似:

public IQueryable<ProductDTO> Get(ProductDTO dto)

您可以查看Asp.net official demo for versioning,它大量使用 DTO 和 AutoMapper,它会给您一个很好的方向,如果您现在不感兴趣,请忽略版本控制。

【讨论】:

这些示例很好,但遗憾的是我看不到转换计算字段(未存储在数据库中但在调用时在内存中计算的字段)。我宁愿不在视图模型中复制我的代码来处理业务状态逻辑。不过,我想向用户公开其中一些计算字段。有什么提示可以让我使用 AutoMapper?【参考方案3】:

尝试使用 AutoMapper,您需要将这些引用添加到您的控制器

using AutoMapper;
using AutoMapper.QueryableExtensions;

你的方法

[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public IQueryable<ObjectDTO> Get()

    return dbContext.Entities.ProjectTo<ObjectDTO>();

在你的全球

protected void Application_Start()

        //Usually in a diff class Mapping.ConfigureDataTransferObjects();
        Mapper.CreateMap<MyEntity, ObjectDTO>();
        Mapper.CreateMap<ObjectDTO, MyEntity>();

【讨论】:

【参考方案4】:

对我来说,解决方案只是将 DTO 添加到 EDM 配置 (v4):

edmBuilder.EntitySet<Contact>("Contacts");
edmBuilder.EntityType<ContactDto>();

【讨论】:

我希望只公开 $metadata 端点中的 DTO。使用 EntityType 而不是 EntitySet 对我有用。谢谢希米。【参考方案5】:

Patrick,您可以从计算的 sourceValue 中填充目标值,例如:

Mapper.CreateMap<Customer, CustomerDTO>()
    .ForMember(dest => dest.InvoiceCount, opt =>
        opt.MapFrom(src => src.Invoices.Count()));

这个例子来自:http://codethug.com/2015/02/13/web-api-deep-dive-dto-transformations-and-automapper-part-5-of-6/

Arturo,如果不是复杂的映射,您可以在 CreateMap 上使用 reverseMap 来做单线。

【讨论】:

以上是关于如何将针对 DTO 的 OData 查询映射到另一个实体?的主要内容,如果未能解决你的问题,请参考以下文章

如何获取 OData 可查询 Web API 端点过滤器并从 DTO 对象映射它?

如何使用 AutoMapper 将 json 请求 dto 中的 OData 枚举字符串映射到实体枚举属性

如何使用dto和C#窗口形式将信息从一种形式传输到另一种形式?

使用Odata基于EF查询DTO

对 DTO 的 ASP.NET WebApi OData 支持

将 IQueryable where 子句从 DTO 映射到实体