I am getting an exception while doing some testing with an OData service that makes use of containment. I'm running Web Api 2.2 / Web Api OData 5.3 / OData Lib 6.8 and using EF 6 to work with SQL Server on the back end. Here are my data models:
[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; }
}
The Parts collection of the Product entity is marked as contained so that the Parts cannot be directly queried, but only queried by going through the Products entity set. The following URLs produce the expected results:
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"
}
]
}
But when I try to get the Supplier from the Parts entity, I get the following exception in the data service:
Products(1)/Parts(3)/Supplier:
Microsoft.OData.Core.ODataException was unhandled by user code
HResult=-2146233079
Message=The target Entity Set of Navigation Property 'Supplier' could not be found. This is most likely an error in the IEdmModel.
Source=Microsoft.OData.Core
StackTrace:
at Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.CreatePropertySegment(ODataPathSegment previous, IEdmProperty property, String queryPortion)
at Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.CreateNextSegment(String text)
at Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.ParsePath(ICollection`1
segments)
at Microsoft.OData.Core.UriParser.Parsers.ODataPathFactory.BindPath(ICollection`1
segments, ODataUriParserConfiguration configuration)
at Microsoft.OData.Core.UriParser.ODataUriParser.ParsePathImplementation()
at Microsoft.OData.Core.UriParser.ODataUriParser.Initialize()
at System.Web.OData.Routing.DefaultODataPathHandler.Parse(IEdmModel model, String serviceRoot, String odataPath, Boolean enableUriTemplateParsing)
at System.Web.OData.Routing.DefaultODataPathHandler.Parse(IEdmModel model, String serviceRoot, String odataPath)...
If I go to the Parts entity and mark the Supplier navigation property as Contained, it works correctly:
[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"
}
]
}
However this does not seem like the proper way to handle this. This would require every entity to know if it could be navigated to from any other entity via a Contained navigation property and then if so to mark all of its navigation properties as Contained as well, so that the full chain could be navigated.
Is this behavior by design? Or is there something else I'm missing that could solve this issue?
EDIT:
As requested, here is the rest of the code in this process: the controllers, the custom routing convention that allows the Parts entity to be Contained, the custom ODataPathHandler that allows us to debug the exception and the WebApiConfig where this is all wired up.
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);
}
}