将 DTO 与 OData 和 Web API 结合使用
Using DTO's with OData & Web API
使用 Web API 和 OData,我有一个公开数据传输对象而不是 Entity Framework 实体的服务。
我使用 AutoMapper 使用 ProjectTo()
:
将 EF 实体转换为其 DTO 计数器部分
public class SalesOrdersController : ODataController
{
private DbContext _DbContext;
public SalesOrdersController(DbContext context)
{
_DbContext = context;
}
[EnableQuery]
public IQueryable<SalesOrderDto> Get(ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config);
}
[EnableQuery]
public IQueryable<SalesOrderDto> Get([FromODataUri] string key, ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.Where(so => so.SalesOrderNumber == key)
.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config);
}
}
AutoMapper (V4.2.1) 配置如下,注意 ExplicitExpansion()
防止序列化自动扩展导航属性,当它们没有被请求时:
cfg.CreateMap<SalesOrderHeader, SalesOrderDto>()
.ForMember(dest => dest.SalesOrderLines, opt => opt.ExplicitExpansion());
cfg.CreateMap<SalesOrderLine, SalesOrderLineDto>()
.ForMember(dest => dest.MasterStockRecord, opt => opt.ExplicitExpansion())
.ForMember(dest => dest.SalesOrderHeader, opt => opt.ExplicitExpansion());
ExplicitExpansion()
然后会产生一个新问题,其中以下请求会引发错误:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines
The query specified in the URI is not valid. The specified type member 'SalesOrderLines' is not supported in LINQ to Entities
导航 属性 SalesOrderLines
对 EF 来说是未知的,所以这个错误几乎是我预期会发生的。问题是,我该如何处理这种类型的请求?
ProjectTo()
方法确实有一个重载,允许我传入一个需要扩展的属性数组,我发现并修改了扩展方法 ToNavigationPropertyArray
以尝试将请求解析为字符串数组:
[EnableQuery]
public IQueryable<SalesOrderDto> Get([FromODataUri] string key, ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.Where(so => so.SalesOrderNumber == key)
.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config, null, queryOptions.ToNavigationPropertyArray());
}
public static string[] ToNavigationPropertyArray(this ODataQueryOptions source)
{
if (source == null) { return new string[]{}; }
var expandProperties = string.IsNullOrWhiteSpace(source.SelectExpand?.RawExpand) ? new List<string>().ToArray() : source.SelectExpand.RawExpand.Split(',');
for (var expandIndex = 0; expandIndex < expandProperties.Length; expandIndex++)
{
// Need to transform the odata syntax for expanding properties to something EF will understand:
// OData may pass something in this form: "SalesOrderLines($expand=MasterStockRecord)";
// But EF wants it like this: "SalesOrderLines.MasterStockRecord";
expandProperties[expandIndex] = expandProperties[expandIndex].Replace(" ", "");
expandProperties[expandIndex] = expandProperties[expandIndex].Replace("($expand=", ".");
expandProperties[expandIndex] = expandProperties[expandIndex].Replace(")", "");
}
var selectProperties = source.SelectExpand == null || string.IsNullOrWhiteSpace(source.SelectExpand.RawSelect) ? new List<string>().ToArray() : source.SelectExpand.RawSelect.Split(',');
//Now do the same for Select (incomplete)
var propertiesToExpand = expandProperties.Union(selectProperties).ToArray();
return propertiesToExpand;
}
这适用于扩展,所以现在我可以处理如下请求:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines
或更复杂的请求,例如:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($expand=MasterStockRecord)
但是,尝试将 $select 与 $expand 组合的更复杂的请求将失败:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($select=OrderQuantity)
Sequence contains no elements
所以,问题是:我的处理方式是否正确?
感觉很臭,我必须写一些东西来解析并将 ODataQueryOptions 转换成 EF 可以理解的东西。
看来这个话题比较热门:
- odata-expand-dtos-and-entity-framework
- how-to-specify-the-shape-of-results-with-webapi2-odata-with-expand
- web-api-queryable-how-to-apply-automapper
- how-do-i-map-an-odata-query-against-a-dto-to-another-entity
虽然其中大部分建议使用 ProjectTo
,但 none 似乎解决了序列化自动扩展属性的问题,或者如果已配置 ExplictExpansion
则如何处理扩展的问题。
类 和配置如下:
Entity Framework (V6.1.3) 实体:
public class SalesOrderHeader
{
public string SalesOrderNumber { get; set; }
public string Alpha { get; set; }
public string Customer { get; set; }
public string Status { get; set; }
public virtual ICollection<SalesOrderLine> SalesOrderLines { get; set; }
}
public class SalesOrderLine
{
public string SalesOrderNumber { get; set; }
public string OrderLineNumber { get; set; }
public string Product { get; set; }
public string Description { get; set; }
public decimal OrderQuantity { get; set; }
public virtual SalesOrderHeader SalesOrderHeader { get; set; }
public virtual MasterStockRecord MasterStockRecord { get; set; }
}
public class MasterStockRecord
{
public string ProductCode { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; }
}
OData (V6.13.0) 数据传输对象:
public class SalesOrderDto
{
[Key]
public string SalesOrderNumber { get; set; }
public string Customer { get; set; }
public string Status { get; set; }
public virtual ICollection<SalesOrderLineDto> SalesOrderLines { get; set; }
}
public class SalesOrderLineDto
{
[Key]
[ForeignKey("SalesOrderHeader")]
public string SalesOrderNumber { get; set; }
[Key]
public string OrderLineNumber { get; set; }
public string LineType { get; set; }
public string Product { get; set; }
public string Description { get; set; }
public decimal OrderQuantity { get; set; }
public virtual SalesOrderDto SalesOrderHeader { get; set; }
public virtual StockDto MasterStockRecord { get; set; }
}
public class StockDto
{
[Key]
public string StockCode { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; }
}
OData 配置:
var builder = new ODataConventionModelBuilder();
builder.EntitySet<StockDto>("Stock");
builder.EntitySet<SalesOrderDto>("SalesOrders");
builder.EntitySet<SalesOrderLineDto>("SalesOrderLines");
我从来没有真正设法解决这个问题。 ToNavigationPropertyArray()
扩展方法有一点帮助,但不能处理无限深度导航。
真正的解决方案是创建 Actions 或 Functions 以允许客户端请求需要更复杂查询的数据。
另一种方法是进行多次 smaller/simple 调用,然后在客户端上聚合数据,但这并不理想。
当您想在 AutoMapper 中标记某些内容以进行显式扩展时,您还需要在调用 ProjectTo<>()
时选择重新加入。
// map
cfg.CreateMap<SalesOrderHeader, SalesOrderDto>()
.ForMember(dest => dest.SalesOrderLines, opt => opt.ExplicitExpansion());
// updated controller
[EnableQuery]
public IQueryable<SalesOrderDto> Get()
{
return _dbContext.SalesOrders
.ProjectTo<SalesOrderDto>(
AutoMapperConfig.Config,
so => so.SalesOrderLines,
// ... additional opt-ins
);
}
虽然 AutoMapper wiki 确实说明了这一点,但该示例可能有点误导,因为它不包括成对的 ExplicitExpansion()
调用。
To control which members are expanded during projection, set ExplicitExpansion in the configuration and then pass in the members you want to explicitly expand:
我已经创建了一个 Automapper 显式导航扩展实用函数,它应该与 N-deph 扩展一起使用。将它张贴在这里,因为它可能会对某人有所帮助。
public List<string> ProcessExpands(IEnumerable<SelectItem> items, string parentNavPath="")
{
var expandedPropsList = new List<String>();
if (items == null) return expandedPropsList;
foreach (var selectItem in items)
{
if (selectItem is ExpandedNavigationSelectItem)
{
var expandItem = selectItem as ExpandedNavigationSelectItem;
var navProperty = expandItem.PathToNavigationProperty?.FirstSegment?.Identifier;
expandedPropsList.Add($"{parentNavPath}{navProperty}");
//go recursively to subproperties
var subExpandList = ProcessExpands(expandItem?.SelectAndExpand?.SelectedItems, $"{parentNavPath}{navProperty}.");
expandedPropsList = expandedPropsList.Concat(subExpandList).ToList();
}
}
return expandedPropsList;
}
你可以调用它:
var navExp = ProcessExpands(options?.SelectExpand?.SelectExpandClause?.SelectedItems)
它将return一个包含["Parent" ,"Parent.Child"]
的列表
使用 Web API 和 OData,我有一个公开数据传输对象而不是 Entity Framework 实体的服务。
我使用 AutoMapper 使用 ProjectTo()
:
public class SalesOrdersController : ODataController
{
private DbContext _DbContext;
public SalesOrdersController(DbContext context)
{
_DbContext = context;
}
[EnableQuery]
public IQueryable<SalesOrderDto> Get(ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config);
}
[EnableQuery]
public IQueryable<SalesOrderDto> Get([FromODataUri] string key, ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.Where(so => so.SalesOrderNumber == key)
.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config);
}
}
AutoMapper (V4.2.1) 配置如下,注意 ExplicitExpansion()
防止序列化自动扩展导航属性,当它们没有被请求时:
cfg.CreateMap<SalesOrderHeader, SalesOrderDto>()
.ForMember(dest => dest.SalesOrderLines, opt => opt.ExplicitExpansion());
cfg.CreateMap<SalesOrderLine, SalesOrderLineDto>()
.ForMember(dest => dest.MasterStockRecord, opt => opt.ExplicitExpansion())
.ForMember(dest => dest.SalesOrderHeader, opt => opt.ExplicitExpansion());
ExplicitExpansion()
然后会产生一个新问题,其中以下请求会引发错误:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines
The query specified in the URI is not valid. The specified type member 'SalesOrderLines' is not supported in LINQ to Entities
导航 属性 SalesOrderLines
对 EF 来说是未知的,所以这个错误几乎是我预期会发生的。问题是,我该如何处理这种类型的请求?
ProjectTo()
方法确实有一个重载,允许我传入一个需要扩展的属性数组,我发现并修改了扩展方法 ToNavigationPropertyArray
以尝试将请求解析为字符串数组:
[EnableQuery]
public IQueryable<SalesOrderDto> Get([FromODataUri] string key, ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.Where(so => so.SalesOrderNumber == key)
.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config, null, queryOptions.ToNavigationPropertyArray());
}
public static string[] ToNavigationPropertyArray(this ODataQueryOptions source)
{
if (source == null) { return new string[]{}; }
var expandProperties = string.IsNullOrWhiteSpace(source.SelectExpand?.RawExpand) ? new List<string>().ToArray() : source.SelectExpand.RawExpand.Split(',');
for (var expandIndex = 0; expandIndex < expandProperties.Length; expandIndex++)
{
// Need to transform the odata syntax for expanding properties to something EF will understand:
// OData may pass something in this form: "SalesOrderLines($expand=MasterStockRecord)";
// But EF wants it like this: "SalesOrderLines.MasterStockRecord";
expandProperties[expandIndex] = expandProperties[expandIndex].Replace(" ", "");
expandProperties[expandIndex] = expandProperties[expandIndex].Replace("($expand=", ".");
expandProperties[expandIndex] = expandProperties[expandIndex].Replace(")", "");
}
var selectProperties = source.SelectExpand == null || string.IsNullOrWhiteSpace(source.SelectExpand.RawSelect) ? new List<string>().ToArray() : source.SelectExpand.RawSelect.Split(',');
//Now do the same for Select (incomplete)
var propertiesToExpand = expandProperties.Union(selectProperties).ToArray();
return propertiesToExpand;
}
这适用于扩展,所以现在我可以处理如下请求:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines
或更复杂的请求,例如:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($expand=MasterStockRecord)
但是,尝试将 $select 与 $expand 组合的更复杂的请求将失败:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($select=OrderQuantity)
Sequence contains no elements
所以,问题是:我的处理方式是否正确? 感觉很臭,我必须写一些东西来解析并将 ODataQueryOptions 转换成 EF 可以理解的东西。
看来这个话题比较热门:
- odata-expand-dtos-and-entity-framework
- how-to-specify-the-shape-of-results-with-webapi2-odata-with-expand
- web-api-queryable-how-to-apply-automapper
- how-do-i-map-an-odata-query-against-a-dto-to-another-entity
虽然其中大部分建议使用 ProjectTo
,但 none 似乎解决了序列化自动扩展属性的问题,或者如果已配置 ExplictExpansion
则如何处理扩展的问题。
类 和配置如下:
Entity Framework (V6.1.3) 实体:
public class SalesOrderHeader
{
public string SalesOrderNumber { get; set; }
public string Alpha { get; set; }
public string Customer { get; set; }
public string Status { get; set; }
public virtual ICollection<SalesOrderLine> SalesOrderLines { get; set; }
}
public class SalesOrderLine
{
public string SalesOrderNumber { get; set; }
public string OrderLineNumber { get; set; }
public string Product { get; set; }
public string Description { get; set; }
public decimal OrderQuantity { get; set; }
public virtual SalesOrderHeader SalesOrderHeader { get; set; }
public virtual MasterStockRecord MasterStockRecord { get; set; }
}
public class MasterStockRecord
{
public string ProductCode { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; }
}
OData (V6.13.0) 数据传输对象:
public class SalesOrderDto
{
[Key]
public string SalesOrderNumber { get; set; }
public string Customer { get; set; }
public string Status { get; set; }
public virtual ICollection<SalesOrderLineDto> SalesOrderLines { get; set; }
}
public class SalesOrderLineDto
{
[Key]
[ForeignKey("SalesOrderHeader")]
public string SalesOrderNumber { get; set; }
[Key]
public string OrderLineNumber { get; set; }
public string LineType { get; set; }
public string Product { get; set; }
public string Description { get; set; }
public decimal OrderQuantity { get; set; }
public virtual SalesOrderDto SalesOrderHeader { get; set; }
public virtual StockDto MasterStockRecord { get; set; }
}
public class StockDto
{
[Key]
public string StockCode { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; }
}
OData 配置:
var builder = new ODataConventionModelBuilder();
builder.EntitySet<StockDto>("Stock");
builder.EntitySet<SalesOrderDto>("SalesOrders");
builder.EntitySet<SalesOrderLineDto>("SalesOrderLines");
我从来没有真正设法解决这个问题。 ToNavigationPropertyArray()
扩展方法有一点帮助,但不能处理无限深度导航。
真正的解决方案是创建 Actions 或 Functions 以允许客户端请求需要更复杂查询的数据。
另一种方法是进行多次 smaller/simple 调用,然后在客户端上聚合数据,但这并不理想。
当您想在 AutoMapper 中标记某些内容以进行显式扩展时,您还需要在调用 ProjectTo<>()
时选择重新加入。
// map
cfg.CreateMap<SalesOrderHeader, SalesOrderDto>()
.ForMember(dest => dest.SalesOrderLines, opt => opt.ExplicitExpansion());
// updated controller
[EnableQuery]
public IQueryable<SalesOrderDto> Get()
{
return _dbContext.SalesOrders
.ProjectTo<SalesOrderDto>(
AutoMapperConfig.Config,
so => so.SalesOrderLines,
// ... additional opt-ins
);
}
虽然 AutoMapper wiki 确实说明了这一点,但该示例可能有点误导,因为它不包括成对的 ExplicitExpansion()
调用。
To control which members are expanded during projection, set ExplicitExpansion in the configuration and then pass in the members you want to explicitly expand:
我已经创建了一个 Automapper 显式导航扩展实用函数,它应该与 N-deph 扩展一起使用。将它张贴在这里,因为它可能会对某人有所帮助。
public List<string> ProcessExpands(IEnumerable<SelectItem> items, string parentNavPath="")
{
var expandedPropsList = new List<String>();
if (items == null) return expandedPropsList;
foreach (var selectItem in items)
{
if (selectItem is ExpandedNavigationSelectItem)
{
var expandItem = selectItem as ExpandedNavigationSelectItem;
var navProperty = expandItem.PathToNavigationProperty?.FirstSegment?.Identifier;
expandedPropsList.Add($"{parentNavPath}{navProperty}");
//go recursively to subproperties
var subExpandList = ProcessExpands(expandItem?.SelectAndExpand?.SelectedItems, $"{parentNavPath}{navProperty}.");
expandedPropsList = expandedPropsList.Concat(subExpandList).ToList();
}
}
return expandedPropsList;
}
你可以调用它:
var navExp = ProcessExpands(options?.SelectExpand?.SelectExpandClause?.SelectedItems)
它将return一个包含["Parent" ,"Parent.Child"]