Sunday, September 7, 2014

Step by step MVC WebAPI with OData support - RESTful HTTP Service ODataController


In this article we'll create an OData RESTful HTTP Service based on the WebAPI  architecture of Asp.Net MVC ODataController, with support for most CRUD operations (Create Read & Delete), complying to OData v4.0 . A smaller tutorial about how to create a Web API OData v3 Service with support for only Retrieve operations (HTTP GET), and inheriting from ApiController, can be seen in this article. The post about setting an OData v4.0 WebAPI with updating support (HTTP PUT & HTTP PATCH) can be found here

For this tutorial, we'll  setup from scratch an MVC Web API and enable it as an OData v4 RESTful  service, to handle HTTP  GET, HTTP POST, and HTTP DELETE requests.   

We'll make an RESTful OData Web API following just 4 steps:
1) create an MVC app & install/update Web API and OData assemblies
2) create the data Model;
3) configure the OData Endpoint at the Register() in the WebApiConfig;
4) create an ODataController and set the "EnableQuery" attribute over the Action Methods

REST is a web architecture which enables handling requests according to its HTTP  verbs: GET will allows retrieving data, HTTP POST allows creating a new item, PUT is for updating ALL of the properties of some  item, while HTTP PATCH is for updating partially some object. Finally, HTTP  DELETE is for erasing an  item.
 
Here we'll use as data model an XML file where the data is kept, and we'll expose it using the OData protocol with its standard operators, and between them we'll support sorting ($orderby)  and paging ($skip & $top) :


1) Step #1 : create the MVC Web API & install Web API and OData .dlls:

First of all, we create a new EMPTY Asp.Net MVC Application:




Then, we need to UPDATE the existing Web API references, so open the NuGet Console and type:

Update-Package Microsoft.AspNet.WebApi -Pre


Next, install the OData NuGet package by typing:

Install-Package Microsoft.AspNet.Odata






2) Step #2 : create the data Model : 


Create the Model as follows:

Next, i will use Data Annotations to make the OData Service to force the fields contents to comply to certain restrictions: soon you will see that the OData service will require the request to comply:

(copy-paste) :

 public class Note
    {
        public int ID { get; set; }
        [Required]
        [RegularExpression(@"^[a-zA-Z''-'\s]{1,20}$", 
            ErrorMessage = "The 'To' field is where you type your username")]
        public string To { get; set; }
        [Required(ErrorMessage = "Please type your email address")]
        [EmailAddress(ErrorMessage = "Your email address seems to be mispelled")]
        [Display(Name = "Email")]
        public string From { get; set; }
        [Required]
        [StringLength(10)]
        public string Heading { get; set; }
        public string Body { get; set; }
        public Note()
        {

        }
        public Note(int ID, string To, string From, string Heading, string Body)
        {
            this.ID = ID;
            this.To = To;
            this.From = From;
            this.Heading = Heading;
            this.Body = Body;
        }
    }


Important: you MUST code a parameterless constructor in your class, because the serializer will need it while rendering the data from the Controller.

Now, we need a Repository and an XML file with the data. Copy-paste them from this short article.


3) Step #3 : configure the OData Endpoint at the WebApiConfig  Register()  :

Set the OData Endpoint at the WebApiConfig Register() method, as follows:


public static void Register(HttpConfiguration config)
        {
            
            ODataModelBuilder builder = new ODataConventionModelBuilder();
            builder.EntitySet<Note>("Notes");
            config.MapODataServiceRoute(
                routeName: "ODataRoute",
                routePrefix: "OData",
                model: builder.GetEdmModel());          

        }

If the Intellisense do not find the method "MapODataServiceRoute" , add the following to the "usings":

using System.Web.OData.Extensions;


The "routePrefix" will state the name of the Service Endpoint. Here we use the ODataConventionModelBuilder to create the service's metadata , but you can customize the Model adding keys or properties , using the ODataModelBuilder instead.

Also, add the following code line at the end of the Global.asax Application_Start:
GlobalConfiguration.Configuration.EnsureInitialized(); 

If at this step you get an error "Error 8 'System.Web.Http.HttpConfiguration' does not contain a definition for 'MapODataServiceRoute'", take a look at this article about how to fix it.

4) Step #4 : build an ODataController and mark with the  "EnableQuery" attribute the Action Methods : 

Important :  Here is mandatory that you call "key" to the Action method's parameters representing the ID of the entities to be fetched. This convention must be respected.

Now create a regular Controller, but make it inherit from ODataController :


Important: the NAME of the Controller MUST be identical to the name of the Model Entity : in our case, we must call the controller "NotesController".

Why do we sign the Action Method as "EnableQuery"? Because that's the key that enables the OData Service:


Important:   The Action method's names MUST be named after the HTTP verbs : Get is HTTP GET, Post is HTTP POST, Patch is HTTP PATCH.....

HTTP GET verb :


Now we obtain the data from the XML Repository, and render an IQueryable<Note> collection :


 [EnableQuery]
        public IQueryable<Note> Get()
        {           
            IEnumerable<Note> data = Repository.GetAll();
            return  Repository.GetAll().AsQueryable<Note>();
        }


At the code above, notice the important points that i remarked with red .

 Build  the service :
Send an HTTP GET request according to the OData protocol :




Important : take care about the text in the URI, since the OData protocol is case-sensitive, meaning that if you write "notes" instead of "Notes", you won't get the data:


As you see, we have paging ($skip & $top) and sorting ($orderby) support : 

We also get automatically support for the most used OData protocol's queries. You can query the OData Web API by means of $metadata, $select, $top, $value, $filter, $skip and $expand , getting an JSON response:
             /OData/$metadata#Notes :


              /Odata/Notes?$filter=From eq 'Fry'





               /OData/Notes?$select=Body


Now we create a method to read ONLY a selected item :



[EnableQuery]
        public SingleResult<Note> Get([FromODataUri]int key)
        {
            IEnumerable<Note> data = Repository.GetAll();
            var note = data.Where(n => n.ID == key).AsQueryable();
            return SingleResult.Create<Note>(note);
        }


Important :  Forgive me but i'll say this again: here it is mandatory that you name "key" to the Action method's parameters representing the ID of the entities to be fetched!!!!!

We got just the selected entity:



As you see, we got the Key for the item using the "FromODataUri" attribute. But what will happen if we name the Key parameter as "id" instead of "key"? It will  leave the OData protocol, and act as a regular RESTful method.
The request will look as this:







HTTP POST verb : 

Now we code a Post() Action Method as follows:



[EnableQuery]
        public  IHttpActionResult Post([FromBody]Note note)
        {
            if (note == null || !ModelState.IsValid )
            {
                return BadRequest(ModelState);
            }
            Repository.Add(note);
            
            return Created(note);

        }



We create a POST request with Fiddler (a short tutorial about Fiddler setup and use can be found here), and send a JSON entity inside the Request Body. Notice we set the "Content-Type" to JSON at the request header.


We get a response containing the entity, because our Post() renders an IHttpActionResult according to the  Created() method.

But if we send a POST request which is not consistent with the Data Annotations we set, it will be rejected:



The request was rejected , and we got a "Bad Request" response with the error message informing us what was illegal:





HTTP DELETE verb : 



 [EnableQuery]
        public IHttpActionResult Delete([FromODataUri] int key)
        {            
            return Repository.Delete(key ) ? 
                StatusCode(HttpStatusCode.Gone)
                StatusCode(HttpStatusCode.NotFound);
        }


Important :  Again, here is mandatory that you call "key"  the Action method's parameters representing the ID of the entities to be deleted.


The HTTP DELETE Request :



The response looks as this:





We got an HTTP "Gone"  response, as everything went OK. If we don't send the entity to be erased, we got a "Bad Request" response error code, as set:



As you see, we got the Key for the item to be deleted using the "FromODataUri" attribute and the "key" name. We can also send a full Entity to be deleted, in the body of the DELETE request, if we set instead of from URI,  "[FromBody]":




As you see, the entire entity was sent inside the request's body.


That's all
In this post we've seen how to create an OData v4.0  RESTful HTTP Service with support for most CRUD  operations using a Web API Asp.Net MVC application. The post about setting an OData v4.0 WebAPI with updating support (HTTP PUT & HTTP PATCH) can be found here.

Enjoy programming.....
    By Carmel Shvartzman
כתב: כרמל שוורצמן