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)
如果你决定要使用的DTO(这绝对是一个好主意,在我看来),然后用它...
的$metadata
应该反映而不是EF实体的DTO的属性名称,因为这是客户得到,这是什么客户应发送。
这意味着您应该将Get
端点更改为以下内容:
public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)
Run Code Online (Sandbox Code Playgroud)
为避免和之间的耦合ProductDTO
,Product
您可以使用AutoMapper为您在类之间进行映射。另外,如果使用AutoMapper的Project
方法,则可以将方法清理为类似以下内容:
public IQueryable<ProductDTO> Get(ProductDTO dto)
Run Code Online (Sandbox Code Playgroud)
您可以查看Asp.net官方演示版本,该版本大量使用DTO和AutoMapper,它将为您提供良好的指导,如果您现在不感兴趣,请忽略该版本。
对于那些使用 .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