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

17

我的问题与这个问题非常相似:如何将针对 DTO 的 OData 查询映射到 EF 实体? 我有一个简单的设置来测试 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解决吧?

6个回答

19

我找到了一种不使用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();
    }
}

聪明!为什么要使用AutoMapper,而不是完全使用C#来完成它。这也可以使用Queryable和Expression Tree完成。 - user1019042
ODataQueryOptions 只支持 $filter、$orderby、$top 和 $skip。 - Signcodeindie
如果有人需要 new ODataQueryContext(model, typeof(TypeName), q.Context.Path),也可以使用 new ODataQueryContext(model, typeof(TypeName), null) - Gilberto Alexandre
嗨@niklr,你能回答这个问题吗?谢谢。https://stackoverflow.com/questions/62650392/net-core-using-odata-in-onion-architecture-mapping-query-parameters-from-dto-t - user13817218

9
如果你决定使用DTOs(在我看来绝对是个好主意),那么就要用它...
$metadata 应该反映出DTO的属性名称而不是EF实体的属性名称,因为这是客户端获取的内容,也是客户端应该发送的内容。
这意味着你应该将 Get 端点更改为以下内容:
public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)

为避免ProductDTOProduct之间的耦合,您可以使用AutoMapper来为您映射类。另外,如果您使用AutoMapper的Project方法,您可以将您的方法整理成类似以下的样子:
public IQueryable<ProductDTO> Get(ProductDTO dto)

您可以查看Asp.net官方版本控制演示,它广泛使用的数据传输对象(DTO)和AutoMapper,它将为您提供很好的指导,如果现在您对版本控制不感兴趣,请忽略它。

2
这些示例很好,但遗憾的是我没有看到绕过转换计算字段(那些不存储在数据库中,而是在调用时在内存中计算)的方法。我不想在视图模型中复制处理业务状态逻辑的代码。我想向用户公开其中一些计算字段。有什么提示可以让我使用AutoMapper吗? - Patrick Michalina

5

如果您正在使用.NET 6和Microsoft.AspNetCore.OData 8.0.8,您可以按照以下方式进行操作:

[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
    private readonly MyDbContext _context;

    public ProductsController(MyDbContext context)
    {
        _context = context; 
    }

    [HttpGet]
    [EnableQuery]
    public IQueryable<ProductDto> Get()
    {
        return _context.Products
            .Select(p => new ProductDTO()
            {
                DisplayName = p.Name,
                DisplayLevel = p.Level
            });
    }
}

在你的创业公司中:

builder.Services
    .AddControllers()
    .AddOData(opt => opt.Filter().Select())

请注意,关键在于投影。将其更改为.Select(p => new ProductDto(p)将不起作用,因为它无法转换为SQL。不再需要EDM模型。
假设有一个名为Products的表,看起来像这样: enter image description here 对于此URL的GET请求: http://localhost:XXXX/products?$filter=DisplayName eq 'Emma'&select=DisplayLevel 结果将是:
[{"DisplayLevel":3}]

将生成类似于以下SQL的内容:

exec sp_executesql N'SELECT [p].[Level], [p].[Name]
FROM [Products] AS [p]
WHERE [p].[Name] = @__TypedProperty_0',N'@__TypedProperty_0 nvarchar(4000)',@__TypedProperty_0=N'Emma'

如SQL中所示,这种方法存在一个缺点,即尽管在URL中的选择仅请求“DisplayLevel”,但整个模型“Name”和“Level”仍从数据库中获取。这可能是由于投影的限制所致。
完整的示例可以在此处找到: https://github.com/smoksnes/ODataExample

但是没有Product.DisplayName,因此$select将无法工作。您需要在实体和DTO之间进行映射。 - Tomas Voracek
@TomasVoracek - 那是一个不幸的错误。我已经更新了代码并添加了更多细节。 - smoksnes

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>();
}

3

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

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

1
我想在$metadata端点中仅公开DTO。使用EntityType<MyDto>而不是EntitySet对我有用。谢谢Shimmy。 - Jonathan Ramos

0

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