Utilizzando Web API e OData, dispongo di un servizio che espone oggetti Data Transfer invece delle entità Entity Framework.Utilizzo di DTO con OData e API Web
che uso Automapper trasformare le Entità EF nelle loro controparti DTO usando 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) è configurato come segue, notare il ExplicitExpansion()
che impedisce serializzazione automatica espansione proprietà di navigazione quando non sono richieste:
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()
quindi crea un nuovo problema in cui la seguente richiesta genera un errore:
/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
La proprietà di spostamento SalesOrderLines
è sconosciuta a EF, quindi questo errore è più o meno quello che mi aspettavo. La domanda è: come gestisco questo tipo di richiesta?
Procedimento ProjectTo()
ha un sovraccarico che mi permette di passare in una serie di proprietà che richiedono l'espansione, ho trovato & modifica il metodo di estensione ToNavigationPropertyArray
per cercare di analizzare la richiesta in una matrice di stringhe:
[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;
}
Questo funziona per espandersi, così ora posso gestire una richiesta simile alla seguente:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines
o una richiesta più complessa come:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($expand=MasterStockRecord)
Tuttavia, la richiesta più complicato che cercano di coniugare $ selezionare con $ espandere fallirà:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($select=OrderQuantity)
Sequence contains no elements
Quindi, la domanda è: sto avvicinando questo il modo giusto? Sembra molto maleodorante dover scrivere qualcosa da analizzare e trasformare ODataQueryOptions in qualcosa che EF possa capire.
Sembra che questo è un argomento piuttosto popolare:
- 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
Mentre la maggior parte di questi suggeriamo di utilizzare ProjectTo
, nessuno sembra affrontare serializzazione auto expan ding properties, o come gestire l'espansione se è stato configurato ExplictExpansion
.
Classi e configurazione di seguito:
Entity Framework entità (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) di trasferimento dati oggetti:
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 Config:
var builder = new ODataConventionModelBuilder();
builder.EntitySet<StockDto>("Stock");
builder.EntitySet<SalesOrderDto>("SalesOrders");
builder.EntitySet<SalesOrderLineDto>("SalesOrderLines");
Ciao Dave, sto già espandendo in modo esplicito le proprietà di navigazione. Il codice sopra mostra il codice che uso per configurarlo e analizzare l'odata per ottenere quali proprietà di navigazione richiedono l'espansione. Questo è lo scopo del metodo di estensione 'ToNavigationPropertyArray()', il cui risultato viene poi passato a 'ProjectTo()'. La mia domanda era/riguarda se utilizzare il metodo di estensione per analizzare la stringa di query odata fosse l'approccio giusto perché diventa complicato se è necessario espandere più proprietà di navigazione. Ci ho lavorato attorno usando azioni e funzioni per query più complicate. – philreed