Adding a custom query backed Navigation Property t

2019-01-15 13:42发布

问题:

Situation

I created the following Model classes

public class Car
{
    public int Id {get;set;}
    public string Name {get;set;}

    public virtual ICollection<PartState> PartStates {get;set; }
}

public class PartState
{
    public int Id {get;set;}
    public string State {get;set;}

    public int CarId {get;set;}
    public virtual Car Car {get;set;}

    public int PartId {get;set;}
    public virtual Part Part {get;set;}
}

public class Part
{
    public int Id {get;set;}
    public string Name {get;set;}
}

And a matching DbContext

public class CarContext : DbContext
{
    public DbSet<Car> Cars {get;set;}
    public DbSet<PartState> PartStates {get;set;}
    public DbSet<Part> Parts {get;set;}
}

And created a WebApplication to make this available via odata, using the scaffolding template "Web API 2 OData Controller with Actions, using Entity Framework"

also i create following webapi config:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var builder = new ODataConventionModelBuilder();
        builder.EntitySet<Car>("Cars");
        builder.EntitySet<PartState>("PartStates");
        builder.EntitySet<Part>("Parts");
        var edmModel = builder.GetEdmModel();
        config.Routes.MapODataRoute("odata", "odata", edmModel);
    }
}

I now want to add the following Method to my Cars Controller

// GET: odata/Cars(5)/Parts
[Queryable]
public IQueryable<Part> GetParts([FromODataUri] int key)
{
    var parts = db.PartStates.Where(s => s.CarId == key).Select(s => s.Part).Distinct();
    return parts;
}

And retrieve the data with this Url:

http://localhost/odata/Cars(1)/Parts

But it does not work, instead i get the following error:

{
  "odata.error":{
    "code":"","message":{
      "lang":"en-US","value":"No HTTP resource was found that matches the request URI 'http://localhost/odata/Cars(1)/Parts'."
    },"innererror":{
      "message":"No routing convention was found to select an action for the OData path with template '~/entityset/key/unresolved'.","type":"","stacktrace":""
    }
  }
}

Question

So my question is, is that even possible?!

I tried to create a Navigation property manually, and added it to the edm model, while this does resolve the issue to invoke the new method, it also introduces new Errors.

EDIT:

What id did try to add it manually in this way:

var edmModel = (EdmModel)builder.GetEdmModel();
var carType = (EdmEntityType)edmModel.FindDeclaredType("Car");
var partType = (EdmEntityType)edmModel.FindDeclaredType("Part");

var partsProperty = new EdmNavigationPropertyInfo();
partsProperty.TargetMultiplicity = EdmMultiplicity.Many;
partsProperty.Target = partType;
partsProperty.ContainsTarget = false;
partsProperty.OnDelete = EdmOnDeleteAction.None;
partsProperty.Name = "Parts";

var carsProperty = new EdmNavigationPropertyInfo();
carsProperty.TargetMultiplicity = EdmMultiplicity.Many;
carsProperty.Target = carType;
carsProperty.ContainsTarget = false;
carsProperty.OnDelete = EdmOnDeleteAction.None;
carsProperty.Name = "Cars";

var nav = EdmNavigationProperty.CreateNavigationPropertyWithPartner(partsProperty, carsProperty);

carType.AddProperty(nav);

config.Routes.MapODataRoute("odata", "odata", edmModel);

while this allowed me to invoke the above speciefied method trough the also above specified URL, it gave me the following error:

{
  "odata.error":{
    "code":"","message":{
      "lang":"en-US","value":"An error has occurred."
    },"innererror":{
      "message":"The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; odata=fullmetadata; charset=utf-8'.","type":"System.InvalidOperationException","stacktrace":"","internalexception":{
        "message":"The related entity set could not be found from the OData path. The related entity set is required to serialize the payload.","type":"System.Runtime.Serialization.SerializationException","stacktrace":"   at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObject(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)\r\n   at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)\r\n   at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()\r\n   at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__1b.MoveNext()"
      }
    }
  }
}

回答1:

You have to call "AddNavigationTarget" on the EntitySet. Assume that your namespace is "MyNamespace", then add the following code to your WebApiConfig.cs. In this way, retrieving the data with "Get: odata/Cars(1)/Parts" will work.

    var cars = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Cars");
    var parts = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Parts");
    var carType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Car");
    var partType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Part");

    var partsProperty = new EdmNavigationPropertyInfo();
    partsProperty.TargetMultiplicity = EdmMultiplicity.Many;
    partsProperty.Target = partType;
    partsProperty.ContainsTarget = false;
    partsProperty.OnDelete = EdmOnDeleteAction.None;
    partsProperty.Name = "Parts";

    cars.AddNavigationTarget(carType.AddUnidirectionalNavigation(partsProperty), parts);


回答2:

Taking @FengZhao's answer further, in order to get the url odata/Cars working you also need to register the navigation property link builder to entity set link builder.

var cars = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Cars");
var parts = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Parts");
var carType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Car");
var partType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Part");

var partsProperty = new EdmNavigationPropertyInfo();
partsProperty.TargetMultiplicity = EdmMultiplicity.Many;
partsProperty.Target = partType;
partsProperty.ContainsTarget = false;
partsProperty.OnDelete = EdmOnDeleteAction.None;
partsProperty.Name = "Parts";

var navigationProperty = carType.AddUnidirectionalNavigation(partsProperty);
cars.AddNavigationTarget(navigationProperty, parts);

var linkBuilder = edmModel.GetEntitySetLinkBuilder(cars);
linkBuilder.AddNavigationPropertyLinkBuilder(navigationProperty, 
    new NavigationLinkBuilder((context, property) =>
        context.GenerateNavigationPropertyLink(property, false), true));


回答3:

Web Api cannot resolve your URL against any of registered URI templates.

Use Route Debugger to figure this out. http://blogs.msdn.com/b/webdev/archive/2013/04/04/debugging-asp-net-web-api-with-route-debugger.aspx

I believe our problem is the id parameter that you pass via url Try making it more explicit

config.Routes.MapHttpRoute
                (
                    "DefaultInternalApi",
                    "api/{controller}/{objectType}/{Id}/{relation}",
                    defaults:
                            new
                            {
                                Id = System.Web.Http.RouteParameter.Optional,
                            }
                );