我在对使用包含的 OData 服务进行一些测试时遇到异常。我正在运行 Web Api 2.2 / Web Api OData 5.3 / OData Lib 6.8 并使用 EF 6 在后端使用 SQL Server。这是我的数据模型:
[Table("Product", Schema = "dbo")]
public class Product
{
public Product()
{
Parts = new HashSet<Part>();
}
[Key]
public int ProductID { get; set; }
[StringLength(100)]
public string ProductName { get; set; }
[Contained]
public virtual ICollection<Part> Parts { get; set; }
}
[Table("Supplier", Schema = "dbo")]
public class Supplier
{
[Key]
public int SupplierID { get; set; }
[StringLength(100)]
public string SupplierName { get; set; }
}
[Table("Part", Schema = "dbo")]
public class Part
{
[Key]
public int PartID { get; set; }
public int ProductID { get; set; }
public int SupplierID { get; set; }
[StringLength(100)]
public string PartName { get; set; }
public virtual Product Product { get; set; }
public virtual Supplier Supplier { get; set; }
}
Product实体的Parts集合被标记为包含,所以Parts不能直接查询,只能通过Products实体集查询。以下 URL 产生预期结果:
Products?$expand=Parts:
{
"@odata.context":"https://localhost:8732/DataApi/$metadata#Products","value":[
{
"ProductID":1,"ProductName":"Road runner trap","Parts@odata.context":"https://localhost:8732/DataApi/$metadata#Products(1)/Parts","Parts":[
{
"PartID":1,"ProductID":1,"SupplierID":1,"PartName":"Spring"
},{
"PartID":2,"ProductID":1,"SupplierID":1,"PartName":"Wire"
},{
"PartID":3,"ProductID":1,"SupplierID":1,"PartName":"Rocket"
}
]
}
]
}
Products(1)/Parts:
{
"@odata.context":"https://localhost:8732/DataApi/$metadata#Products(1)/Parts","value":[
{
"PartID":1,"ProductID":1,"SupplierID":1,"PartName":"Spring"
},{
"PartID":2,"ProductID":1,"SupplierID":1,"PartName":"Wire"
},{
"PartID":3,"ProductID":1,"SupplierID":1,"PartName":"Rocket"
}
]
}
Products(1)/Parts(3):
{
"@odata.context":"https://localhost:8732/DataApi/$metadata#Products(1)/Parts","value":[
{
"PartID":3,"ProductID":1,"SupplierID":1,"PartName":"Rocket"
}
]
}
Suppliers(1):
{
"@odata.context":"https://localhost:8732/DataApi/$metadata#Suppliers","value":[
{
"SupplierID":1,"SupplierName":"Acme Industries"
}
]
}
但是当我尝试从 Parts 实体获取 Supplier 时,我在数据服务中得到以下异常:
产品(1)/零件(3)/供应商:
用户代码未处理 Microsoft.OData.Core.ODataException
HResult=-2146233079
消息=找不到导航属性“供应商”的目标实体集。这很可能是 IEdmModel 中的错误。
Source=Microsoft.OData.Core
StackTrace:在 Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.CreateNextSegment 上的 Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.CreatePropertySegment
(
ODataPathSegment 以前,IEdmProperty 属性,字符串 queryPortion)
)
在 Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.ParsePath( ICollection`1
segments)
在 Microsoft.OData.Core.UriParser.Parsers.ODataPathFactory.BindPath( ICollection`1
segments, ODataUriParserConfiguration 配置)
在 Microsoft.OData.Core.UriParser.ODataUriParser.ParsePathImplementation()
在 Microsoft.OData.Core.UriParser.ODataUriParser.Initialize()
在 System.Web.OData.Routing.DefaultODataPathHandler.Parse(IEdmModel 模型,字符串 serviceRoot,字符串 odataPath, Boolean enableUriTemplateParsing)
在 System.Web.OData.Routing.DefaultODataPathHandler.Parse(IEdmModel 模型,字符串 serviceRoot,字符串 odataPath)...
如果我转到 Parts 实体并将供应商导航属性标记为包含,它可以正常工作:
[Table("Part", Schema = "dbo")]
public class Part
{
[Key]
public int PartID { get; set; }
public int ProductID { get; set; }
public int SupplierID { get; set; }
[StringLength(100)]
public string PartName { get; set; }
public virtual Product Product { get; set; }
[Contained]
public virtual Supplier Supplier { get; set; }
}
Products(1)/Parts(3)/Supplier:
{
"@odata.context":"https://localhost:8732/DataApi/$metadata#Products(1)/Parts(3)/Supplier","value":[
{
"SupplierID":1,"SupplierName":"Acme Industries"
}
]
}
但是,这似乎不是处理此问题的正确方法。这将要求每个实体知道它是否可以通过包含导航属性从任何其他实体导航到,然后如果可以,则将其所有导航属性也标记为包含,以便可以导航整个链。
这种行为是设计使然吗?还是我还缺少其他可以解决此问题的东西?
编辑:
根据要求,这里是此过程中的其余代码:控制器、允许包含 Parts 实体的自定义路由约定、允许我们调试异常的自定义 ODataPathHandler 以及所有连接的 WebApiConfig。
public class ProductsController : ODataController
{
ProductsContext db = new ProductsContext();
[EnableQuery]
public IQueryable<Product> Get()
{
return db.Products;
}
[EnableQuery]
public IQueryable<Product> Get(int key)
{
return db.Products.Where(p => p.ProductID == key);
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
public class SuppliersController : ODataController
{
ProductsContext db = new ProductsContext();
[EnableQuery]
public IQueryable<Supplier> Get()
{
return db.Suppliers;
}
[EnableQuery]
public IQueryable<Supplier> Get(int key)
{
return db.Suppliers.Where(s => s.SupplierID == key);
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
public class PartsController : ODataController
{
ProductsContext db = new ProductsContext();
[EnableQuery]
public IQueryable<Part> GetFromRelatedEntity(int relatedEntityKey)
{
return db.Parts.Where(p => p.ProductID == relatedEntityKey);
}
[EnableQuery]
public IQueryable<Part> GetFromRelatedEntity(int relatedEntityKey, int key)
{
return db.Parts.Where(p => p.PartID == key && p.ProductID == relatedEntityKey);
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
public class RelatedEntityRoutingConvention : IODataRoutingConvention
{
public string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
{
if (odataPath.PathTemplate == "~/entityset/key/navigation")
{
string actionName = controllerContext.Request.Method.Method + "FromRelatedEntity";
if (actionMap.Contains(actionName))
{
var keyValueSegment = odataPath.Segments[1] as KeyValuePathSegment;
controllerContext.RouteData.Values["relatedEntityKey"] = keyValueSegment.Value;
return actionName;
}
}
else if (odataPath.PathTemplate == "~/entityset/key/navigation/key")
{
string actionName = controllerContext.Request.Method.Method + "FromRelatedEntity";
if (actionMap.Contains(actionName))
{
var firstKeyValueSegment = odataPath.Segments[1] as KeyValuePathSegment;
var secondKeyValueSegment = odataPath.Segments[3] as KeyValuePathSegment;
controllerContext.RouteData.Values["relatedEntityKey"] = firstKeyValueSegment.Value;
controllerContext.RouteData.Values["key"] = secondKeyValueSegment.Value;
return actionName;
}
}
return null;
}
public string SelectController(ODataPath odataPath, HttpRequestMessage request)
{
if (odataPath.PathTemplate == "~/entityset/key/navigation" || odataPath.PathTemplate == "~/entityset/key/navigation/key")
return odataPath.NavigationSource.Name;
return null;
}
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
builder.EntitySet<Supplier>("Suppliers");
List<IODataRoutingConvention> myRoutingConventions = new List<IODataRoutingConvention>();
myRoutingConventions.Add(new RelatedEntityRoutingConvention());
myRoutingConventions.AddRange(ODataRoutingConventions.CreateDefault());
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
model: builder.GetEdmModel(),
pathHandler: new MyODataPathHandler(),
routingConventions: myRoutingConventions,
batchHandler: new EntityFrameworkBatchHandler(new HttpServer(config))
);
}
}
public class MyODataPathHandler : DefaultODataPathHandler
{
// This is where the exception occurs.
public override ODataPath Parse(IEdmModel model, string serviceRoot, string odataPath)
{
return base.Parse(model, serviceRoot, odataPath);
}
}