Использование 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 может понять.

Кажется, это довольно популярная тема:

Хотя большинство из них предлагают использовать 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 в конфигурации и затем передайте элементы, которые вы хотите явно развернуть:

Другие вопросы по тегам