將 DTO 與 OData 和 Web API 一起使用
使用 Web API 和 OData,我有一個服務,它公開數據傳輸對象而不是實體框架實體。
我使用 AutoMapper 將 EF 實體轉換為它們的 DTO 計數器部分,使用
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
URI 中指定的查詢無效。LINQ to Entities 不支持指定的類型成員“SalesOrderLines”
導航屬性
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( $ 展開=MasterStockRecord)
但是,嘗試合併的更複雜的請求 $ select with $ 展開將失敗:
/odatademo/SalesOrders(‘123456’)? $ expand=SalesOrderLines( $ 選擇=訂單數量)
序列不包含任何元素
所以,問題是:我以正確的方式接近這個嗎?感覺很臭,我必須寫一些東西來解析 ODataQueryOptions 並將其轉換為 EF 可以理解的東西。
這似乎是一個相當流行的話題:
- odata-expand-dtos-and-entity-framework
- 如何指定結果的形狀與 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如何處理擴展。ExplictExpansion下面的類和配置:
實體框架 (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 以允許客戶端請求需要更複雜查詢的數據。
另一種選擇是進行多個較小/簡單的呼叫,然後在客戶端聚合數據,但這並不理想。
我創建了一個 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)它將返回一個列表
["Parent" ,"Parent.Child"]