I'm trying to put together a simple toy project using Entity Framework, WebAPI, OData, and an Angular client. Everything is working fine, except the navigation property that I have put on one of my models doesn't seem to be working. When I call my API using $expand, the returned entities do not have their navigation properties.
My classes are Dog and Owner, and look like this:
public class Dog
{
// Properties
[Key]
public Guid Id { get; set; }
public String Name { get; set; }
[Required]
public DogBreed Breed { get; set; }
public int Age { get; set; }
public int Weight { get; set; }
// Foreign Keys
[ForeignKey("Owner")]
public Guid OwnerId { get; set; }
// Navigation
public virtual Owner Owner { get; set; }
}
public class Owner
{
// Properties
public Guid Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string Phone { get; set; }
public DateTime SignupDate { get; set; }
// Navigation
public virtual ICollection<Dog> Dogs { get; set; }
}
I also have my Dog controller set up to handle querying:
public class DogsController : ODataController
{
DogHotelAPIContext db = new DogHotelAPIContext();
#region Public methods
[Queryable(AllowedQueryOptions = System.Web.Http.OData.Query.AllowedQueryOptions.All)]
public IQueryable<Dog> Get()
{
var result = db.Dogs.AsQueryable();
return result;
}
[Queryable(AllowedQueryOptions = System.Web.Http.OData.Query.AllowedQueryOptions.All)]
public SingleResult<Dog> Get([FromODataUri] Guid key)
{
IQueryable<Dog> result = db.Dogs.Where(d => d.Id == key).AsQueryable().Include("Owner");
return SingleResult.Create(result);
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
I've seeded the database with a bit of sample data. All dog records have an OwnerId that matches the Id of an Owner in the Owners table.
Querying for the list of dogs using this works fine:
http://localhost:49382/odata/Dogs
I get a list of Dog entities, without the Owner navigation property.
Querying for the dogs with their owners using OData $expand does NOT work:
http://localhost:49382/odata/Dogs?$expand=Owner
My response is a 200 with all of the Dog entities, but none of them have an Owner property on them in the JSON.
If I query my metadata, I find that OData does seem to know about it:
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="DogHotelAPI.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Dog">
<Key>
<PropertyRef Name="id" />
</Key>
<Property Name="id" Type="Edm.Guid" Nullable="false" />
<Property Name="name" Type="Edm.String" />
<Property Name="breed" Type="DogHotelAPI.Models.Enums.DogBreed" Nullable="false" />
<Property Name="age" Type="Edm.Int32" Nullable="false" />
<Property Name="weight" Type="Edm.Int32" Nullable="false" />
<Property Name="ownerId" Type="Edm.Guid" />
<NavigationProperty Name="owner" Type="DogHotelAPI.Models.Owner">
<ReferentialConstraint Property="ownerId" ReferencedProperty="id" />
</NavigationProperty>
</EntityType>
<EntityType Name="Owner">
<Key>
<PropertyRef Name="id" />
</Key>
<Property Name="id" Type="Edm.Guid" Nullable="false" />
<Property Name="name" Type="Edm.String" />
<Property Name="address" Type="Edm.String" />
<Property Name="phone" Type="Edm.String" />
<Property Name="signupDate" Type="Edm.DateTimeOffset" Nullable="false" />
<NavigationProperty Name="dogs" Type="Collection(DogHotelAPI.Models.Dog)" />
</EntityType>
</Schema>
<Schema Namespace="DogHotelAPI.Models.Enums" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EnumType Name="DogBreed">
<Member Name="AfghanHound" Value="0" />
<Member Name="AmericanStaffordshireTerrier" Value="1" />
<Member Name="Boxer" Value="2" />
<Member Name="Chihuahua" Value="3" />
<Member Name="Dachsund" Value="4" />
<Member Name="GermanShepherd" Value="5" />
<Member Name="GoldenRetriever" Value="6" />
<Member Name="Greyhound" Value="7" />
<Member Name="ItalianGreyhound" Value="8" />
<Member Name="Labrador" Value="9" />
<Member Name="Pomeranian" Value="10" />
<Member Name="Poodle" Value="11" />
<Member Name="ToyPoodle" Value="12" />
<Member Name="ShihTzu" Value="13" />
<Member Name="YorkshireTerrier" Value="14" />
</EnumType>
</Schema>
<Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityContainer Name="Container">
<EntitySet Name="Dogs" EntityType="DogHotelAPI.Models.Dog">
<NavigationPropertyBinding Path="owner" Target="Owners" />
</EntitySet>
<EntitySet Name="Owners" EntityType="DogHotelAPI.Models.Owner">
<NavigationPropertyBinding Path="dogs" Target="Dogs" />
</EntitySet>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
What could I be missing that is preventing my navigation preoprty from coming back with the rest of my model?
EDIT
To further isolate the problem I tried including the Owners in C# on the server side. I added this line in the Get method of my Dog controller:
var test = db.Dogs.Include("Owner").ToList();
With this I can debug and see that the related owners ARE being included. Each dog has the owner that is associated with it in this list.
Using .Include("Owner") on what is actually returned does not fix the problem - the properties still never reach the client.
This seems to mean that the navigation properties are working, but are not being sent back to the client. This seems like it wound indicate an issue with OData or WebAPI, I would guess, but I'm not sure what.
Also, I have added the following lines to Application_Start in my Global.asax file in order to handle circular navigation properties:
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling =
Newtonsoft.Json.PreserveReferencesHandling.All;
I did that in case a circular reference was somehow the culprit, but this changes nothing.
UPDATE
I noticed that making a call to
http://localhost:49382/odata/Dogs(abfd26a5-14d8-4b14-adbe-0a0c0ef392a7)/owner
works. This retrieves the owner associated with that dog. This further illustrates that my navigation properties are set up correctly, they just aren't being included in responses to calls using $expand.
UPDATE 2
Here is the register method of my WebApiConfig file:
public static void Register(HttpConfiguration config)
{
//config.Routes.MapHttpRoute(
// name: "DefaultApi",
// routeTemplate: "api/{controller}/{id}",
// defaults: new { id = RouteParameter.Optional }
//);
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EnableLowerCamelCase();
builder.EntitySet<Dog>("Dogs");
builder.EntitySet<Owner>("Owners");
config.EnableQuerySupport();
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: "odata",
model: builder.GetEdmModel());
// Uncomment the following line of code to enable query support for actions with an IQueryable or IQueryable<T> return type.
// To avoid processing unexpected or malicious queries, use the validation settings on QueryableAttribute to validate incoming queries.
// For more information, visit http://go.microsoft.com/fwlink/?LinkId=279712.
//config.EnableQuerySupport();
// To disable tracing in your application, please comment out or remove the following line of code
// For more information, refer to: http://www.asp.net/web-api
config.EnableSystemDiagnosticsTracing();
}
I found the solution to my problem, which was ultimately caused by three things:
1.) I was using the [Queryable] attribute on my controller methods, which are deprecated. I needed to use the newer [EnableQuery] attributes.
2.) In my WebApiConfig.cs file I was enabling querying by using the default config.EnableQuerySupport(). This is deprecated, and has been removed.
3.) My expand call needed was in the form of $expand=Owner but needed to be in the form of $expand=owner since I am enabling lower camel case on my ODataConventionModelBuilder. Thank you very much to Mark Bennetts, whose answer pointed this out!
After making all of these changes, related Owner entities are being returned with Dog entities.
I had a very similar problem, which I believe is caused by the exact same issue.
I was trying to create some bound OData functions which would return entire graphs of entities to make the clients job a little easier in certain situations rather than having to specify $expand clauses for everything.
I specified Include statements in the entity framework linq calls, and verified that the return data was indeed fully populated in debug but, like you I was only ever getting the top-level entity returned with nothing else.
The problem lies with the serialisation used for OData
What you'll find is that if you remove the primary key from your Owner class so it essentially becomes a complex entity, then it will be included in the OData serialised JSON result, otherwise it will not unless the OData request uri comprises an $expand clause that includes it.
I tried to find a way to insert $expand clauses in code to make it work, but unfortunately came up blank.
Hope this helps
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With