Использование DTO с OData & Web API
Используя веб-API и OData, у меня есть служба, которая предоставляет объекты передачи данных вместо сущностей Entity Framework.
Я использую 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')? $ развернуть =SalesOrderLines
Запрос, указанный в URI, недопустим. Указанный член типа 'SalesOrderLines' не поддерживается в 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')? $ развернуть =SalesOrderLines
или более сложный запрос, такой как:
/ odatademo / SalesOrders ('123456')? $ = расширение SalesOrderLines($ расширение =MasterStockRecord)
Однако более сложный запрос, который попытается объединить $select с $expand, завершится неудачно:
/ odatademo / SalesOrders ('123456')? $ развернуть =SalesOrderLines($ выберите =OrderQuantity)
Последовательность не содержит элементов
Итак, вопрос: правильно ли я подхожу к этому? Очень неприятно, что мне нужно написать что-то для анализа и преобразования ODataQueryOptions в то, что EF может понять.
Кажется, это довольно популярная тема:
- OData расширяющегося-DTOS-и-сущность-рамка
- как к указать, в-образную форму из-результаты-с-webapi2-OData-с-расширения
- Веб-апи-запрашиваемых-хау к применять-automapper
- как-д-я-карта-ан-OData-запросы к-а-DTO-к-другому ничтожество
Хотя большинство из них предлагают использовать ProjectTo
ни один, кажется, не обращаются к свойствам автоматического расширения сериализации, или как обращаться с расширением, если 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 Config:
var builder = new ODataConventionModelBuilder();
builder.EntitySet<StockDto>("Stock");
builder.EntitySet<SalesOrderDto>("SalesOrders");
builder.EntitySet<SalesOrderLineDto>("SalesOrderLines");
3 ответа
Мне так и не удалось разобраться с этим. ToNavigationPropertyArray()
Метод расширения немного помогает, но не справляется с бесконечной глубиной навигации.
Реальным решением является создание действий или функций, позволяющих клиентам запрашивать данные, требующие более сложного запроса.
Другая альтернатива - сделать несколько небольших / простых вызовов, а затем объединить данные на клиенте, но это не совсем идеально.
Я создал утилиту явного расширения навигации 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, $"{navProperty}.");
expandedPropsList = expandedPropsList.Concat(subExpandList).ToList();
}
}
return expandedPropsList;
}
Вы можете позвонить с помощью:
var navExp = ProcessExpands(options?.SelectExpand?.SelectExpandClause?.SelectedItems)
он вернет список с ["Parent" ,"Parent.Child"]
Если вы хотите пометить что-то для явного расширения в 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()
вызов.
Чтобы контролировать, какие члены раскрываются во время проецирования, установите ExplicitExpansion в конфигурации и затем передайте элементы, которые вы хотите явно развернуть: