Asp.net

將 DTO 與 OData 和 Web API 一起使用

  • April 11, 2019

使用 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 可以理解的東西。

這似乎是一個相當流行的話題:

雖然其中大多數建議使用,但似乎沒有一個解決序列化自動擴展屬性,或者如果已配置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"]

引用自:https://stackoverflow.com/questions/36284142