Getting started with ASP.NET Web API 2.2 for OData v4.0

Getting started with ASP.NET Web API 2.2 for OData v4.0

Rate This
  • Comments 33

A few weeks ago we started publishing nightly builds for our initial support in ASP.NET Web API for the OData v4.0 protocol. Our OData v4.0 support is based on the OData Library for OData v4.0 that has been released in the past few months. The OData v4.0 protocol introduces a lot of changes and new features that allow more flexibility in the way to model services and improvements over many features from the past versions of the protocol.

In addition to this, the OData protocol has been recently ratified as an OASIS standard which will help bolster the adoption of the protocol by many companies and services all over the internet. If you want to know more about OData you can check the official site at www.odata.org where you can find the complete specification of the protocol and the features, the different formats supported and information about existing OData clients you can use in your apps. If you want to take a sneak peak at the new features and changes in the v4.0 version, you can do it here.

During the past few months, the Web API team has been working on the initial support for the v4.0 version. Many of the existing changes in the current nightly build deal with protocol and format changes from the v3.0 to the v4.0 version, but we have managed to add some interesting features to our current OData support. This list of features include:

1. OData attribute routing: This feature allows you to define the routes in your controllers and actions using attributes.

2. Support for functions: This feature allows you to define functions in your OData model and bind them to actions in your controller that implement them.

3. Model aliasing: This feature allows to change the names of the types and properties in your OData model to be different than the ones in your CLR types.

4. Support for limiting allowed queries: This feature allows the service to define limitations on the properties of the model that can be filtered, sorted, expanded or navigated across.

5. Support for ETags: This feature allows to generate an @odata.etag annotation based on some properties of the entity that can be used in IfMatch and IfNoneMatch headers in following requests.

6. Support for Enums: We’ve improved our support for Enums and now we support them as OData enumerations.

7. Support for $format: We’ve also added support for $format, so clients are able to specify the desired format of the response in the URL.

Important changes in this version

The OData v4.0 protocol includes a lot of new features and many changes to existing ones that improve the protocol and the modeling capabilities for the services implementers, but at the same time, those changes make difficult to support multiple versions of the protocol in a single implementation.

For that reason, we have decided to create a new assembly to support the v4.0 version of the protocol while maintaining the current assembly for those people who want to implement services based on previous versions.

One of the important goals with this new implementation has been to support the side by side scenario where customers can have v3 and v4 services running on the same application. To that effect, we had to make some changes in the current naming of some classes and methods to allow for a reasonable user experience. Here are the most important changes:

1. The package ID for the v4.0 is Microsoft.AspNet.OData.

2. The assembly name and the root namespace are now System.Web.OData instead of System.Web.Http.OData.

3. All the extension methods have been moved to System.Web.OData.Extensions.

4. We have removed all the extension methods that used to exist for HttpRequestMessage like GetODataPath or GetEdmModel and we have added a single extension method, ODataProperties that returns an object containing the common OData properties that were accessible by the old extension methods, like the IEdmModel of the service or the ODataPath of the request.

5. MapODataRoute has been changed to MapODataServiceRoute.

6. QueryableAttribute has been changed to EnableQueryAttribute.

For the sake of consistency between versions, we have done the same set of changes in the Microsoft.AspNet.WebApi.OData to achieve a similar development experience. Only the namespace remains System.Web.Http.OData in this version. The current methods and class names can still be used with the System.Web.Http.OData (OData v3.0), but we have marked them as obsolete, and they are not available in the new assembly.

Enough talking, let’s write an OData v4.0 service!

We’ll start our new OData v4.0 service by creating a simple web application that we’ll call ODataV4Service. We’ll chose to use the Web API template that will install the default Web API packages required for our application.

Once the basic application has been created, the first thing we need to do is update the existing Web API packages to use the nightly versions hosted on MyGet. In order to do that, right click on “References” in the project we have just created on the solution explorer, click on “Manage Nuget Packages” and expand the Updates section on the left.

image

Check that there is a source for WebStack Nightly, and if not, just proceed to add it by clicking the Settings button on the left bottom corner of the window and adding the source in the windows that appears after clicking, as shown in the following figure.

clip_image004

As you can see from the image, the URL for the nightly ASP.NET packages is http://www.myget.org/f/aspnetwebstacknightly/ and you can see all the different published packages on https://www.myget.org/gallery/aspnetwebstacknightly.

Now that we have setup our nightly package source we can go and update the Web API packages. In order to do that, we need to select the Include Prerelease option on the dropdown menu on the top of the window. Then we just need to click Update All.

Before leaving the Nuget Package Manager we need to install the Web API 2.2 for OData v4.0 package, in order to do that, we expand the Online tab, select the WebStack Nightly Source and the Include Prerelease option and then search for Microsoft.AspNet.OData.

clip_image006

After installing this package, we can exit the Nuget Package Manager and try running our application by pressing F5. The default page should appear in the browser.

At this point we have our application running on the latest 5.2 assemblies and we are ready to create our OData service. The first step is to create a model, for that we create a couple of C# classes representing entities as follow:

public class Player
{
 
    public virtual int Id { get; set; }
 
    public virtual int TeamId { get; set; }
 
    public virtual string Name { get; set; }
}
 
public class Team
{
    public virtual int Id { get; set; }
    
    public virtual string Name { get; set; }
    
    public virtual double Rate { get; set; }
    
    public virtual int Version { get; set; }
    
    public virtual ICollection<Player> Players { get; set; }
 
    public Category Category { get; set; }
}

We are going to need some data to use, so we are going to use Entity Framework for that, in order to do that, we install the Entity Framework package from Nuget in the same way we have done with the OData package, except this time we pick the nuget.org package source and a stable version of the package. Then we create a context and include an initializer to seed the database with some data, as shown here:

 

public class LeagueContext : DbContext
{
    public DbSet<Team> Teams { get; set; }
    public DbSet<Player> Players { get; set; }

    static LeagueContext()
    {
        Database.SetInitializer<LeagueContext>(new LeagueContextInitializer());
    }

    private class LeagueContextInitializer : DropCreateDatabaseAlways<LeagueContext>
    {
        protected override void Seed(LeagueContext context)
        {
            context.Teams.AddRange(Enumerable.Range(1, 30).Select(i =>
                new Team
                {
                    Id = i,
                    Name = "Team " + i,
                    Rate = i * Math.PI / 10,
                    Players = Enumerable.Range(1, 11).Select(j =>
                        new Player
                        {
                            Id = 11 * (i - 1) + j,
                            TeamId = i,
                            Name = string.Format("Team {0} Player {1}", i, j)
                        }).ToList()
                }
            ));
        }
    }
}

The next step is creating our OData model. We are going to create it in the WebApiConfig.cs file as the next figure shows:

 

public static IEdmModel GetModel()
{
    ODataModelBuilder builder = new ODataConventionModelBuilder();

    builder.EntitySet<Team>("Teams");
    builder.EntitySet<Player>("Players");

    return builder.GetEdmModel();
}

OData attribute routing

Now that we have created our model, we need to define the route for the OData service. We are going to use OData Attribute Routing to define the routes in our service. In order to do that, we need to open the WebApiConfig.cs file under our App_Start folder and add the System.Web.OData.Extensions and System.Web.OData.Routing namespaces to the list of our usings. Then, we need to modify our Register method to add the following lines:

ODataRoute route = config.Routes.MapODataServiceRoute("odata", "odata",GetModel());
route.MapODataRouteAttributes(config);

At this point we have successfully configured our OData service, but we haven’t defined yet any controller to handle the incoming requests. Ideally we would use scaffolding for this, but we are still working on getting the OData v4.0 scaffolders ready for preview (the existing scaffolders only support OData v3.0 services). So we have to create our controllers by hand, but we’ll see that with attribute routing it’s not difficult at all.

In previous versions of our Web API OData support, we had a very tight restriction on the names of the controllers, actions and even parameter names of our actions. With attribute routing, all those restrictions go away. We can define a controller or an action using whatever name we want as the following fragment of code shows:

 

[ODataRoutePrefix("Teams")]
public class TeamsEntitySetController : ODataController
{
    private readonly LeageContext _leage = new LeageContext();
 
    [EnableQuery]
    [ODataRoute]
    public IHttpActionResult GetFeed()
    {
        return Ok(_leage.Teams);
    }
    [ODataRoute("({id})")]
    [EnableQuery]
    public IHttpActionResult GetEntity(int id)
    {
        return Ok(SingleResult.Create<Team>(_leage.Teams.Where(t => t.Id == id)));
    }
}

As we can see on the figure above, we can use ODataRoutePrefixAttribute to specify a prefix for all the routes in the actions on the controller, and we can use ODataRouteAttribute to specify further segments that will get combined with the ones in the prefix. That way, the GetFeed action, represents the route /Teams and the GetEntity action represents routes like Teams(1), Teams(2), etc.

Support for Functions

Now that we have a basic service up and running, we are going to introduce some business logic. For that, we are going to define a function that will give us the teams whose rating is around a certain threshold with a given tolerance.

Obviously, we could achieve the same result with a query, but in that case, the clients of our service are ones responsible for defining the query and might make mistakes. However, if we give them a function, they only need to care about sending the right parameters.

In order to define a function that represents the business logic that we have specified, we can modify our GetModel function as follows:

 

public static IEdmModel GetModel()
{
    ODataModelBuilder builder = new ODataConventionModelBuilder();
 
    EntitySetConfiguration<Team> teams = builder.EntitySet<Team>("Teams");
    builder.EntitySet<Player>("Players");
 
    FunctionConfiguration withScore = teams
        .EntityType
        .Collection
        .Function("WithScore");
    withScore.Parameter<double>("threshold");
    withScore.Parameter<double>("tolerance");
    withScore.ReturnsCollectionFromEntitySet<Team>("Teams");
 
    return builder.GetEdmModel();
}

Functions can be defined at the service level (unbounded), at the collection level (bounded to collection) or at the entity level (bounded to the entity). In this case, we have defined a function bounded to the collection, but similar methods exist on the ODataModelBuilder class (to define service level functions) and on the EntityConfiguration class (to define entity level functions).

Now, the last step is to define an action that implements the function, in order to do that, we are going to take advantage of attribute routing. The action in the figure below shows the implementation:

 

[ODataRoute("Default.WithScore(threshold={threshold},tolerance={tolerance})")]
[EnableQuery]
public IHttpActionResult GetTeamsWithScore(double threshold, double tolerance)
{
    return Ok(_league.Teams.Where(t =>
        (t.Rate < (threshold + tolerance)) &&
        (t.Rate > (threshold - tolerance))));
}

As you can see, the way we call the function is by using it’s fully qualified name after the entity set on which we want to call it. We use attribute routing to define the parameters of the function and bind them to the parameters of the action in a very elegant way. In this case a sample call to the function would use the following URL /odata/Teams/Default.WithScore(threshold=3, tolerance=2)

Important note: If you try this in IIS, you’ll probably get a 404 response. This is because IIS doesn’t like the dot in the URL on the last segment (IIS thinks it´s a file). One possible way to fix this is to add piece of configuration on you web.config to ensure IIS runs the routing module on all the requests.

<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"></modules>
</system.webServer>

Model aliasing

So far we’ve seen attribute routing and functions, now we are going to show another very interesting feature, model aliasing. Many times we want to expose some data from our domain, but we want to change things like the names of the domain entities or the names of some properties. In order to do that, we can use model aliasing.

There are two ways to configure model aliasing in our model, we can do it directly through the model builder by setting the name property of the types and the properties of the types, or we can annotate our types with DataContract and DataMember attribute. For example, we can change our model using data contract in the following way:

[DataContract(Name = "Member")]
public class Player
{
    [DataMember]
    public virtual int Id { get; set; }
    [DataMember(Name = "Team")]
    public virtual int TeamId { get; set; }
    [DataMember]
    public virtual string Name { get; set; }
}

Support for limiting the set of allowed queries

As we said above, query limitations allow a service to limit the types of queries that users can issue to our service by imposing limitations on the properties of the types of the model. A service can decide to limit the ability to sort, filter, expand or navigate any property of any type on the model.

In order to do that, there are two options, we can use attributes like Unsortable, NonFilterable, NotExpandable or NotNavigable on the properties of the types in our model, or we can configure this explicitly in the model builder. In this case, we’ll do it though attributes.

 

public class Team
{
    public virtual int Id { get; set; }
    [Unsortable]
    public virtual string Name { get; set; }
    [NonFilterable]
    public virtual double Rate { get; set; }
    [NotExpandable]
    [NotNavigable]
    public virtual ICollection<Player> Players { get; set; }
}

The meaning of Unsortable, NonFilterable and NotExpandable is self-explanatory, as for NotNavigable it is a shortcut for specifying that a property is Unsortable and NonFilterable. When a client issues a query that involves a limited property, the server will answer with a 400 status code and will indicate the limited property that is causing the request to fail.

Support for ETags

The next feature we are going to see is ETags. This feature allows a service to define what fields of an entity are part of the concurrency check for the entity. Those fields will be used to generate an @odata.etag annotation that will be sent to the clients when returning the entity, either as part of a feed or just the single entity.

The client can use this ETag value in the If-Match and If-None-Match headers to implement optimistic concurrency updates and efficient resource caching. In order to mark a field as part of the concurrency check, we can use the ConcurrencyCheck attribute or the Timestamp attribute.

It’s important to note that we should use one or another, but not both at the same time. The difference strives in that ConcurrencyCheck is applicable to multiple fields of the entity and Timestamp is meant to be applied to a single field.

The individual properties can also be marked as part of the concurrency check explicitly using the model builder. In this case, we’ll do it through attributes. For example, we have modified the Team entity to add a Version property and mark it as part of the ETag for the entity. The result is shown in the next figure:

public class Team
{
    public virtual int Id { get; set; }
    [Unsortable]
    public virtual string Name { get; set; }
    [NonFilterable]
    public virtual double Rate { get; set; }
    [ConcurrencyCheck]
    public int Version { get; set; }
    [NotExpandable]
    [NotNavigable]
    public virtual ICollection<Player> Players { get; set; }
}

Now, we will serialize the ETag of the entity when we retrieve it through a GET request, but we still need to take advantage of the ETag on the actions of our service. In order to do that, we are going to add a Put action and we’ll bind ODataQueryOptions<Team> in order to use the ETag.

[ODataRoute("({id})")]
public IHttpActionResult Put(int id, Team team, ODataQueryOptions<Team> options)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
 
    if (id != team.Id)
    {
        return BadRequest("The key on the team must match the key on the url");
    }
 
    if (options.IfMatch != null &&
        !(options.IfMatch.ApplyTo(_leage.Teams.Where(t => t.Id == id))
        as IQueryable<Team>).Any())
    {
        return StatusCode(HttpStatusCode.PreconditionFailed);
    }
    else
    {
        _leage.Entry(team).State = EntityState.Modified;
        _leage.SaveChanges();
        return Updated(team);
    }
}

As we can see, we can take advantage of the ETag by binding ODataQueryOptions as a parameter and using the IfMatch or IfNoneMatch properties in that object in order to apply the ETag value to a given query.

In the above example, we check if the ETag on the IfMatch header exists and if so, if it doesn’t the value of the Team with the id represented by the URL to return a Precondition Failed status in that case.

Support for Enums

We already had support for Enums in Web API OData v3.0 by serializing them as strings, but the new version of the protocol has added full support for them, so we have upgraded our Enum support accordingly. In order to use Enums you just need to define a property with an Enum type and we’ll represent it as an Enum in the $metadata and the clients will able to use Enum query operators in $filter clauses. There are also specific overloads on the model builder in case we want to configure the enumeration explicitly. Defining an OData Enum property in your type is as simple as this:

public enum Category
{
    Amateur,
    Professional
}

public class Team
{
    public virtual int Id { get; set; }
    
    [Unsortable]
    public virtual string Name { get; set; }
    
    [NonFilterable]
    public virtual double Rate { get; set; }
    
    [ConcurrencyCheck]
    public virtual int Version { get; set; }
    
    [NotExpandable]
    [NotNavigable]
    public virtual ICollection<Player> Players { get; set; }

    public Category Category { get; set; }
}

Support for $format

This feature allows a client to specify the format they want in the query string of the URL bypassing any value set by the accept header. For example, the user can issue queries like this one to get all the metadata in the response, instead of just the minimal ammount (which is the default):

http://localhost:12345/odata/Teams?$format=application/json;odata.metadata=full

The above query uses a MIME media type and includes parameters in order to ask for a specific JSON version.

http://localhost:12345/odata/Teams?$format=json

The above query uses an alias to refer to a specific MIME media type, application/json which in the case of OData is equivalent to application/json;odata.metadata=minimal

Using the .NET OData client to query the v4.0 service

The OData client for .NET has been released this week, the following blog post contains the instructions on how to use it to generate a client that can be used to query Web API for OData v4.0 services.

Note: If you plan to use $batch it won’t work properly with the client that gets generated by default. This is caused due to the fact that we are still using the beta version of OData Lib (we plan to update to the RTM version in the near future) and the client uses the RTM version of OData Lib. In order to workaround this issue, you can do the following:

Open the Nuget Package Console and downgrade the OData Client package to beta1 doing:

1. Uninstall-package Microsoft.OData.Client -RemoveDependencies -project <ProjectName>

2. Install-package Microsoft.OData.Client -version 6.0.0-beta1 -pre -project <ProjectName>

Perform the following changes on the T4 template mentioned on the blog:

1. Replace Microsoft.OData.Client.Key with Microsoft.OData.Service.Common.DataServiceKeyAttribute

2. Replace Microsoft.OData.Client.ODataProtocolVersion with Microsoft.OData.Service.Common.DataServiceProtocolVersion

Samples

Along with the nightly build we have published samples with the new features and new samples showing off common scenarios that target OData v4 in the ASP.NET codeplex site.

Conclusion

We have started publishing nightly builds for our OData v4.0 support that it’s built on top of the ODataLib support that has already shipped. Our nightly builds can be found on http://www.myget.org/f/aspnetwebstacknightly. The package ID to find them is Microsoft.AspNet.OData (Remember to select IncludePrerelease in the Nuget Package Manager).

We’ve also seen a brief introduction to all the new features in this version, like OData attribute routing, functions, model aliasing, query limitations, ETags, enums and $format. You can find samples for all the new features in https://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/OData/v4/.

Enjoy!

  • Hi, Thank you so much... I have a question... how can I specify which properties will return when I query this :

    route /Teams ... I just want it to return Name and Rate properties, not all them... Thanks...

  • @Kourosh You can do Team?$select=Name,Rate. The action for the route needs to have the EnableQuery attribute

  • It's great to see model aliasing.

    But the another important part of this feature is an ability to use case-insensitive for properties and functions.

    Would be useful if the dev.team will provide a separate adapter for property binding while OData Request is being parsed.

    Is this feature will be delivered with next release neither with some explanation how it can be done?  

  • @Dmitriy, we are still exploring the best way of to do the case-insensitive feature. odata is clearly a case sensitive protocol. e.g. you have both 'Name' and 'name' for different properties or one property and one function in your model.

  • Just one question: I do not like closing the context when releasing the controller. How we can close the context per action?

  • Can we implement OData 4.0 using WCF Data Service?

    If yes, how?

    If not, is the Web API 2.2 only way to go with?

  • I would like to use WebAPI to expose tabular data as well as OLAP data as OData.   Is there a simple way of gen'ing the EDM for these models -  the reason for this is we create these cubes on the fly and the data model changes as new attributes/ and or dimensions could be added.  

    thank you for any feedback.

  • I'm using Microsoft.AspNet.WebApi.OData v5.2.0-alpha1-14401 in my host and it references ODataLib, Edm and Spatial 5.6.0 (both in the NuGet dialog dependencies list and what it automatically downloads). See the screenshot at http://ge.tt/5UwJY2W1/v/0?c  When I remove the 5.6.0 nuget packages and instead use use only the latest 6.2.0 packages, the Web API Host doesn't recognize the location of IEdmModel in these newer packages and my controller scaffolding doesn't work.  This post states "The assembly name and the root namespace are now System.Web.OData instead of System.Web.Http.OData." Web API 2.2 shouldn't support ODataLib, Edm and Spatial 5.6.0 correct?  Does this post need to be updated to reflect the latest nightly builds on MyGet?

  • Works pretty good! Don't know if it is EF6.1 or the new OData libs - but results with $top & $skip seem much faster.  A few things I noticed using todays build (did not test prior builds):

    1) EF DateTime properties don't get persisted correctly in the JSON - this is a serious bug that pretty much makes it unusable for me until it is fixed.

    2) Some of the new v4 syntax is implemented (like $count=true instead of $inlinecount=allpages) but some is not (like $filter=contains(PropName,'value')) - contains doesn't show up in AllowedFunctions yet.

    Keep up the good work.

  • Getting an exeption "Object not set to an instance of an object"

    In your Global.asax.cs file, configure Web API like this:

    protected void Application_Start()

           {

               GlobalConfiguration.Configure(WebApiConfig.Register);

           }

    Previously, the project templates used this:

    protected void Application_Start()

    {

       // Caution: Won't work with attribute routing

       WebApiConfig.Register(GlobalConfiguration.Configuration);

    }

    forums.asp.net/.../5545668.aspx

  • @Bjorn, please ensure you installed the latest nuget packages.  e.g. stackoverflow.com/.../globalconfiguration-configure-not-present-after-web-api-2-and-net-4-5-1-migra

  • ODataRoute route = config.Routes.MapODataServiceRoute("odata", "odata",GetModel());

    route.MapODataRouteAttributes(config);

    When adding these lines to the Register method of WebApiConfig.cs file, got an error which says

    "'System.Web.Http.HttpRouteCollection' does not contain a definition for 'MapODataServiceRoute' and no extension method 'MapODataServiceRoute' accepting a first argument of type 'System.Web.Http.HttpRouteCollection' could be found (are you missing a using directive or assembly reference?)"

    Can anyone pls help me resolve that?

  • Regarding "'System.Web.Http.HttpRouteCollection' does not contain a definition for 'MapODataServiceRoute' - make sure you have the necessary using statements:

    using System;

    using System.Collections.Generic;

    using System.Data.Entity;

    using System.Linq;

    using System.Web.Http;

    using System.Web.OData.Builder;

    using System.Web.OData.Extensions;

    using System.Web.OData.Routing;

    using System.Web.OData.Routing.Conventions;

    using Microsoft.OData.Edm;

  • I tied to use the T4 template to generate the proxy but it created a new type called ".Nullable_1OfDateTime" when it should have been nullable datetime. I get the following error when I try to query the proxy "The complex type 'System.Nullable_1OfDateTime' has no settable properties." If I manually query the Web API it works fine when generating the json. The same Web API service worked fine with an odata v3 client before I upgraded it to a odata v4 service. The template seems to have problems with nullable datetime.

    Also noted in the comments here: aspnetwebstack.codeplex.com/.../1753

  • The old DataService Client was generating nullable DateTime like this. <Property Name="ShipmentDate" Type="Edm.DateTime" /> but the new T4 template is generating it like this <Property Name="ShipmentDate" Type="System.Nullable_1OfDateTime" />. If you try to replace all the Nullable_1OfDateTime with Edm.DateTime you will get this error "The complex type 'System.Nullable`1[System.DateTime]' has no settable properties."

Page 1 of 3 (33 items) 123