使用DTO在OData和Web API中

13
使用 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中指定的查询无效。指定的类型成员“SalesOrderLines”在LINQ to Entities中不受支持。

EF不知道导航属性SalesOrderLines,所以这个错误正是我预期发生的。问题是,我该如何处理这种类型的请求? 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($expand=MasterStockRecord)

但是,试图将 $select 与 $expand 结合的更复杂的请求将失败:

/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($select=OrderQuantity)

Sequence contains no elements

所以,问题是:我是否正确地处理了这个问题? 我觉得必须编写一些代码来解析和转换 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 配置:

var builder = new ODataConventionModelBuilder();

builder.EntitySet<StockDto>("Stock");
builder.EntitySet<SalesOrderDto>("SalesOrders");
builder.EntitySet<SalesOrderLineDto>("SalesOrderLines");
3个回答

3

我创建了一个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"]


注意:在传递给AutoMapper的ProjectTo()方法之前,记得将结果转换为数组格式并使用ToArray()方法,因为它接受参数数组作为输入。 - Pagotti
你的扩展仅适用于2级嵌套@Ovidiu Buligan。当我有更多深度(孙子)时,级别不会被预置。 - Sebastian

1
我从未真正弄清楚这个问题。 ToNavigationPropertyArray() 扩展方法有所帮助,但不能处理无限深度导航。
真正的解决方案是创建操作或函数,允许客户端请求需要更复杂查询的数据。
另一种选择是进行多个较小/简单的调用,然后在客户端聚合数据,但这并不理想。

0

当您想在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然后传入您想要显式展开的成员:


嗨,戴夫,我已经明确地扩展了导航属性。上面的代码显示了我用来配置此项并解析OData以获取需要扩展哪些导航属性的代码。这就是ToNavigationPropertyArray()扩展方法的目的,其结果随后传递给ProjectTo()。我的问题是/仍然是关于使用扩展方法解析OData查询字符串是否是正确的方法,因为如果需要扩展多个导航属性,它会变得混乱。我通过使用操作和函数来处理更复杂的查询来解决了这个问题。 - philreed

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接