Part 10 – OData v4 in ASP.NET WebApi – Alternate Key

This is a series of blogs detailing my introduction to using OData v4 with C# and ASP.NET WebApi.

The source code for the project can be found here.

Conventional Data Model

Up to now our two code examples built the Entity Data Model (EDM) using the Conventional Data Model.

We defined a model class and with a key attribute and used the ODataConventionModelBuilder class to render a model from our CLR class.

This class is the easiest way of created an EDM and should be used in preference to the alternative classes however; not all functionality can be described using it.

For example; the only attributes that can be added to our model class that are used by the conventional data model are:

[NotMapped]

Property will not be included in the EDM

[Required]

Makes a property non nullable

[Key]

Designate a Single Primary Key

[AutoExpand]

Attribute on navigation property to make it automatically expand without $expand query option

[MaxLength(n)]

Declares maximum length of the property

[NotFilterable] or [NonFilterable]

Cannot use the $filter query option

[NotSortable] or [NonSortable]

Cannot use the $orderby query options

[NotNavigable]

Cannot use the $select query option

[NotExpandable]

Cannot use the $expand query option

[NotCountable]

Cannot use the $count query option

[ForeignKey(“)]

Used to build referential constraint

[ActionOnDelete]

Used to build referential constraint action on delete

[ConcurrencyCheck]

Used for entity updates

[Timestamp]

Used for entity updates

[ComplexType]

Used to mark a class as a complex type

[DataContract] and [DataMember]

Only DataMembers are added to the EDM

 

In this blog we will look at using alternate keys and these cannot be expressed using the conventional model. For these we need to use the explicit model builder.

Explicit Model Builder

There are three different classes for building EDM’s

  • Explicit Model – Class EdmModel
  • Non-conventional Model – Class ODataModelBuilder, inherits from EdmModel
  • Conventional Model – Class ODataConventionModelBuilder, inherits from ODataModelBuilder

You can see that each model is inheriting from the one above. EdmModel comes from the Microsoft.OData.Edm namespace and has the lowest level of abstraction. This means there tends to be more code to write but handles all functionality.

Web API OData 6.x and 5.x

I am using version Web API OData 6.1.0 and Alternate Keys were introduced in version v5.7. Unfortunately Microsoft changed the Api in v6.x and it is not backward compatible. So when looking at examples on the web you need to be mindful about the version.

Always look at the new features for major version at the bottom of the documentation here.

Creating the Project

The Project is called Part10

Create the Car Model

In the previous blogs I created a Car class and added a bunch of attributes however; using the explicit model, it does not render the EDM from a CLR object. Therefore our Car class looks like the following.

public class Car
{
   public string Make { get; set; }
   public string Model { get; set; }
   public string Colour { get; set; }
   public double Price { get; set; }
   public string Category { get; set; }
}

There are no attributes however; for this demo I have added a Category property which will be our Alternate Key.

Create a Data Source

A folder called DataSource was created and within it a class called Repository.

Three methods were created. One to fetch all the cars, one to fetch a specific make and model and another to fetch all cars using the Category alternate key.

public class Repository
{
   private List _cars;

   public Repository()
   {
      _cars = new List {
         new Car {Make = "Vauxhall", Model = "Zafira", Colour = "Blue", Price = 20000.00, Category = "SUV"},
         new Car {Make = "Vauxhall", Model = "Mokka", Colour = "Silver", Price = 18500.00, Category = "4x4"},
         new Car {Make = "Ford", Model = "Focus", Colour = "Black", Price = 22000.00, Category = "HatchBack"},
         new Car {Make = "Peugeot", Model = "3008", Colour = "Green", Price = 19000.00, Category = "4x4"}
      };
   }

   public IQueryable GetCars()
   {
      return _cars.AsQueryable();
   }

   public Car GetCar(string make, string model)
   {
      return _cars.Where(p => p.Make == make && p.Model == model).FirstOrDefault();
   }

   public IQueryable GetCars(string category)
   {
      return _cars.Where(p => p.Category == category).AsQueryable();
   }
}

Create the Entity Data Model (EDM)

This is where things get a whole lot more complicated from how we previously created the model.

private static IEdmModel GetEdmModel()
{
   EdmModel _builder = new EdmModel();

   // Create Car Entity
   EdmEntityType car = new EdmEntityType("Part10.Models", "Car");
   car.AddKeys(car.AddStructuralProperty("Make", EdmPrimitiveTypeKind.String), car.AddStructuralProperty("Model", EdmPrimitiveTypeKind.String));
   car.AddStructuralProperty("Colour", EdmPrimitiveTypeKind.String);
   car.AddStructuralProperty("Price", EdmPrimitiveTypeKind.Double);
   var cat = car.AddStructuralProperty("Category", EdmPrimitiveTypeKind.String, false);
   _builder.AddAlternateKeyAnnotation(car, new Dictionary
   {
      {"Category", cat}
   });
   _builder.AddElement(car);

   // Create Container
   EdmEntityContainer container = new EdmEntityContainer("default", "Container");

   // Add the Cars Entity into the Container
   EdmEntitySet cars = container.AddEntitySet("Cars", car);
   _builder.AddElement(container);

   return _builder;
}

If you follow the code and the properties in the Car class, it should be intuitive as to what each declaration is doing. What is not intuitive is the namespace declaration in the EdmEntityType object.

The Car class is in the .NET namespace of Part10.Models and if I add anything different to the EdmEntityType, Postman will return an error.

Create Cars Controller

The EDM we created above declared an EntitySet called Cars. This means we must create a controller called CarsController. Within the controller we need to support three actions, one to fetch all the cars, another to fetch a car by make and model and one to fetch cars by category.

public class CarsController : ODataController
{
   private Repository _repo;

   public CarsController()
   {
      _repo = new Repository();
   }

   // OData/Cars
   public IQueryable Get()
   {
      return _repo.GetCars();
   }

   // OData/Cars(Make='Vauxhall', Model='Zafira')
   public Car Get([FromODataUri] string KeyMake, [FromODataUri] string KeyModel)
   {
      return _repo.GetCar(KeyMake, KeyModel);
   }

   // OData/Cars(Category='4x4')
   [ODataRoute("Cars(Category={Category})")]
   public IQueryable GetCarByCategory([FromODataUri]string Category)
   {
      return _repo.GetCars(Category);
   }
}

If you followed the previous blogs the first two methods are the same. If you look at the method GetCarByCategory it has an attribute called ODataRoute. Remember I mentioned that actions are found if we follow conventions.

When creating actions for alternate keys, there is no convention and our method will not be found. So in order for this url http:///Cars(Category=’4×4′) to work, we need to decorate the action for this route.

Create an OData Route

This is another area that has significantly changed in v6.x. They moved away from using extension methods on HttpConfiguration and have now created a fluent Api hanging off MapODataServiceRoute.

public static void Register(HttpConfiguration config)
{
   var model = GetEdmModel();

   config.MapODataServiceRoute("OData", "OData", b => b
      .AddService(ServiceLifetime.Singleton, s => model)
      .AddService(ServiceLifetime.Singleton, s => ODataRoutingConventions.CreateDefaultWithAttributeRouting("OData", config))
      .AddService(ServiceLifetime.Singleton, s => new AlternateKeysODataUriResolver(model))
   );
}

You will notice there are a number of additional methods being called. v6.x has integrated with a Dependency Injection (DI) framework.

It uses AddService to register each piece of functionality with a generic type specifying the type of service required.

The first parameter is the lifetime of that service and is typical in all DI implementations. It can be Singleton, Transient or Scoped.

  • Singleton – Created the first time used and subsequent request will use the same instance.
  • Transient – Created each time they’re requested.
  • Scoped – Created once per request.

The second parameter is a delegate pointing to the service.

  • IEdmModel – This is our model and is mandatory.
  • IODataRoutingConvention – Used to determine what Controller and Action will be used for an OData request. If we added CreateDefault, the ODataRoute attribute would be ignored. So adding CreateDefaultWithAttributeRouting is the same as CreateDefault, with the addition of attribute routing.
  • ODataUriResolver – If we added just ODataUriResolver as a service we would get an error when accessing the alternate key Category. It would say, you have not included Make and Model keys. AlternateKeysODataUriResolver adds functionality to deal with this.

Query One Car using our Keys Make and Model

From Postman issue a GET request with http://localhost:40000/OData/Cars(Make=’Vauxhall’,Model=’Zafira’) to fetch a single car.

As you can see this query continues to work fine.

Query All Cars using our Alternate key Category

From Postman issue a GET request with http://localhost:40000/OData/Cars(Category=’4×4′) to fetch all Cars in the 4×4 category.

In the real world where you will be accessing a database backend, alternate keys would be on properties that have a corresponding database column. This column should have an index to keep I/O to a minimum. It’s fair to say that you would not want to have an alternate key on every property.

 

In the next blog I will look a bit closer at the route attributes.