使用DTO& OData& Web API

时间:2016-03-29 12:21:17

标签: asp.net entity-framework asp.net-web-api odata automapper

使用Web API和OData,我有一个服务,它公开数据传输对象而不是实体框架实体。

我使用AutoMapper使用ProjectTo()将EF实体转换为其DTO计数器部分:

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表(&#39; 123456&#39;)?$扩大= SalesOrderLines

     

URI中指定的查询无效。指定的类型成员&#39; SalesOrderLines&#39; LINQ to Entities

不支持

导航属性SalesOrderLines对于EF来说是未知的,因此这个错误几乎与我预期的一样。问题是,我该如何处理这类请求?

ProjectTo()方法确实有一个重载,允许我传入一个需要扩展的属性数组,我发现&amp;修改了扩展方法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表(&#39; 123456&#39;)?$扩大= SalesOrderLines

或更复杂的请求,如:

  

/ odatademo / SalesOrders表(&#39; 123456&#39;)?$扩大= SalesOrderLines($扩大= MasterStockRecord)

然而,尝试将$ select与$ expand结合起来的更复杂的请求将失败:

  

/ odatademo / SalesOrders表(&#39; 123456&#39;)?$扩大= SalesOrderLines($选择= OrderQuantity)

     

序列不包含元素

所以,问题是:我是以正确的方式接近这个吗? 感觉非常臭我必须写一些东西来解析并将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");

3 个答案:

答案 0 :(得分:1)

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

的列表

答案 1 :(得分:0)

我从未真正设法解决这个问题。 ToNavigationPropertyArray()扩展方法略有帮助,但无法处理无限深度导航。

真正的解决方案是创建动作或函数,以允许客户端请求需要更复杂查询的数据。

另一种选择是进行多个较小/简单的调用然后在客户端上聚合数据,但这并不是很理想。

答案 2 :(得分: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,然后传入要显式展开的成员: