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

nik*_*klr 12 c# odata asp.net-web-api

我的问题与此问题非常类似:如何将针对DTO的OData查询映射到EF实体? 我有一个简单的设置来测试ASP.NET Web API OData V4 $过滤器功能.我想做的是"别名"ProductDTO的一些属性以匹配Product实体的属性.用户将使用以下请求调用ProductsController:

GET产品?$ filter = DisplayName eq'test'

产品类:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Level { get; set; }
    public Product()
    { }
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

ProductsController:

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));
    }
}
Run Code Online (Sandbox Code Playgroud)

当然我得到以下异常:

无法在"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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我想我错误地使用了别名功能,因为抛出了与上述相同的异常.如果我调用以下请求它可以工作,但这不是我想要实现的:

GET产品?$ filter =名称eq'test'

更新:

我同意gdoron,Get端点应该如下所示:

public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)
Run Code Online (Sandbox Code Playgroud)

但是如果没有AutoMapper,这应该是可以解决的吗?

nik*_*klr 15

我找到了一个没有使用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));
    }
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)


gdo*_*ica 5

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

public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)
Run Code Online (Sandbox Code Playgroud)

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

public IQueryable<ProductDTO> Get(ProductDTO dto)
Run Code Online (Sandbox Code Playgroud)

您可以查看Asp.net官方演示版本,该版本大量使用DTO和AutoMapper,它将为您提供良好的指导,如果您现在不感兴趣,请忽略该版本。

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

smo*_*nes 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
            });
    }
}
Run Code Online (Sandbox Code Playgroud)

在你的启动中:

builder.Services
    .AddControllers()
    .AddOData(opt => opt.Filter().Select())
Run Code Online (Sandbox Code Playgroud)

请注意,关键在于投影。将其更改为.Select(p => new ProductDto(p)不起作用,因为它无法转换为 SQL。不再需要 EDM 模型。

鉴于有一个名为Products如下所示的表:

在此输入图像描述

GET此网址的A :

http://localhost:XXXX/products?$filter=DisplayName eq 'Emma'&select=DisplayLevel

结果将是:

[{"DisplayLevel":3}]
Run Code Online (Sandbox Code Playgroud)

将生成如下所示的 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'
Run Code Online (Sandbox Code Playgroud)

从 SQL 中可以看出,这种方法有一个缺点,那就是整个模型Name都是Level从数据库中获取的,尽管 url 中的 select 仅请求DisplayLevel。这可能是由于投影造成的限制。

完整的示例可以在这里找到: https: //github.com/smoksnes/ODataExample