Introduction to .NET Web API 2 with C# Part 2

Introduction

We finished the first part of this series with creating an in-memory data source and a Get() API method. The Get method returned the full list of Rockband records in the data repository.

We’re ready to move on to other aspects of this technology. So open the demo project we started previously. We’re going to look at the following elements:

  • Action method: get element by ID
  • Routing by attributes
  • Returning IHttpActionResult from controller action
  • Cross origin resource sharing

Get an element by id

Now we can retrieve all elements in from the API in an XML string. We also want to be able to get a single item by its ID. Open RockbandsController.cs and add the following stub:

public RockBand Get(int id)
{

}

We’ll be able to reach this action by the URL /api/rockbands/{id}. It couldn’t be easier. The most obvious way is to add the following method to InMemoryDatabaseObjectContext.cs:

public RockBand GetById(int id)
{
            return (from r in _rockBands where r.Id == id select r).FirstOrDefault();
}

The body of the Get(id) method will be as follows:

return _objectContextFactory.Create().GetById(id);

Run the application and navigate to /api/rockbands/1. You should get the XML representation of the rockband whose ID is 1:

Get by id XML

What happens if you type an ID of a nonexistent rockband, such as 7? You’ll get a null XML back:

Null XML of non-existent ID

Let’s make this a bit more obvious to the caller. Change the signature of the Get(id) method to return an HttpResponseMessage instead of a rockband:

public HttpResponseMessage Get(int id)

This object allows us to control the response message we return to the caller of the Get(id) method: the response code, the headers, the message content, i.e. all elements of a standard HTTP response. It is customary to let Web API actions return this object. In the body of the action we can return a 404 if the item wasn’t found. Otherwise we return a 200 OK with the Rockband object. Here’s one way to solve this:

public HttpResponseMessage Get(int id)
{
            RockBand rockband = _objectContextFactory.Create().GetById(id);
            if (rockband == null)
            {
                return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No such rockband");
            }
            return Request.CreateResponse<RockBand>(HttpStatusCode.OK, rockband);
}

Re-run the demo app, try to get a rockband with an ID of 7 and you should get a XML formatted error message:

Xml formatted error message

The next operation that would be nice to implement is to retrieve the albums of a rockband by the URL api/rockbands/3/albums. Routing of such URLs in the previous version of the Web API was a bit tricky. You could set up your custom routes in WebApiConfig.cs, but it could get messy if you wanted to support extended urls beyond the basic /api/{controller}/{id} format. However, in Web API 2 there’s a different way.

Routing by attributes

There’s a new built-in feature in web api which lets you declare the routing rules by standard C# attributes. It started its life as a NuGet package but now it’s readily available in .NET. The attributes can be applied on the controller level as well. Examples:

[Route("rockbands/{id}/albums")] //nested route
[Route("rockbands/{id:int:min(1)}"] //constraint on the incoming ID: must be an integer and has a minimum value of 1
[Route("rockbands/{id?}"] //apply to entire controller. The default rules will apply to every method in the controller
[RoutePrefix("api/patients")] //prefix for all routes in controller

You can even declare multiple routes for a single action:

[Route("rockbands/{id:int}")]
[Route("rockbands/{name:hello}"]

Let’s see if can get the api/rockbands/{id}/albums url to work.

Add the following action to RockbandsController:

public HttpResponseMessage GetAlbums(int id)
{
            RockBand rockband = _objectContextFactory.Create().GetById(id);
            if (rockband == null)
            {
                return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No such rockband");
            }
            return Request.CreateResponse<IEnumerable<Album>>(HttpStatusCode.OK, rockband.Albums);
}

Re-run the app and see if the api/rockbands/1 URL still works. It doesn’t. The problem is that we have two methods in the Controller whose name starts with “get” and accept a parameter of type int. The routing engine doesn’t know which one to call.

Inspect the WebApiConfig.cs file. Note the following call:

config.MapHttpAttributeRoutes();

This call comes before the standard…

config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }

…declaration. This will ensure that the routes defined in the attributes will take precedence: if the request URL matches one of the attribute URLs then the routing engine will not search any longer.

Add the following attribute over the GetAlbums action:

[Route("api/rockbands/{id:int:min(1)}/albums")]

Run the app again. First check if api/rockbands/1 works and it should. Now go to api/rockbands/1/albums and you should be presented with the XML representation of the albums of rockband 1. Also, try with an invalid ID, such as api/rockbands/0/albums. You should get a 404.0 error which tells you that the URL wasn’t found. This is because of the constraint we added to the attribute.

Returning IHttpActionResult

We mentioned above that the HttpResponseMessage object is a standard object to return from Web API actions. Web Api 2 introduces another type of object that you can return from an API action: IHttpActionResult. It is similar to ActionResult in standard ASP.NET MVC. In MVC you can return JsonResult, ContentResult and View objects. With this new return type in the Web API you can return similar objects that are specific to HTTP traffic:

  • OkResult
  • NotFoundResult
  • ExceptionResult
  • UnauthorizedResult
  • BadRequestResult
  • ConflictResult
  • RedirectResult
  • InvalidModelStateResult

…and many more. You can even create your custom Result object by deriving from an existing concrete object that implements the IHttpActionResult interface or by implementing the interface directly. Let’s see how we can transform our current code.

Let change…

public HttpResponseMessage Get(int id)

…to…

public IHttpActionResult Get(int id)

The code will not compile any more of course as the HttpResponseMessage object doesn’t implement this interface. It’s incredibly easy to send a NotFount and Ok response. Change the body of Get(id) to the following:

public IHttpActionResult Get(int id)
{
            RockBand rockband = _objectContextFactory.Create().GetById(id);
            if (rockband == null)
            {
                return NotFound();
            }
            return Ok<RockBand>(rockband);
}

We can change GetAlbums to the following:

public IHttpActionResult GetAlbums(int id)
{
            RockBand rockband = _objectContextFactory.Create().GetById(id);
            if (rockband == null)
            {
                return NotFound();
            }
            return Ok<IEnumerable<Album>>(rockband.Albums);
}

CORS: cross origin resource sharing

CORS allows JavaScript to call an API on a domain different from its origin. Normally this has been forbidden as it is considered a security risk. Nowadays the browser can let such a call go through as long as the target server allows the access. The server will let the browser know in a HTTP header if it accepts the HTTP request. The HTTP request must include an Origin header specifying the value where it’s coming from. If the server allows the access then the HTTP response will include an Access-Control-Allow-Origin header which will have the same value as the Origin header in the request, such as http://www.microsoft.com or some other URL. This header tells the browser that it’s OK to access the requested resource from the specified origin. If this header is missing then the browser will not allow JavaScript to receive the response.

Open up another instance of Visual Studio and create a new ASP.NET Web application in called CorsDemo. In the New ASP.NET Project window select the Empty template as we won’t do much with this project. We’ll only use it as a dummy web site on a different origin. Right click this new project and add a new HTML Page called Default.html. Right-click the References tab and click Manage NuGet Packages. Search for the jQuery package and install it.

Open Default.html and add the following code in between the body tags:

<h1>CORS demo</h1>

    <button id="getRockbands">Get rockbands</button>

    <pre id="rockbands">

    </pre>

    <script src="Scripts/jquery-2.1.0.js"></script>
    <script>

    </script>

I hope this is not too complex. We’ll fill in the contents of our own JS within the script tags:

$(function ()
        {
            var rockbandSourceUrl = "http://localhost:50170/api/rockbands/";

            var getRockbands = function ()
            {
                $.get(rockbandSourceUrl).always(showRockbands);
                return false;
            };

            var showRockbands = function (obj)
            {
                $("#rockbands").text(JSON.stringify(obj, null, 3));
            };

            $("#getRockbands").click(getRockbands);
        });

Make sure to enter the correct port number for the rockbandSourceUrl URL. We simply want to retrieve the list of rockbands from the API and show the output in the “rockbands” element. Note that this simple web page will run on a different port so it is considered a different site from the browser’s point of view. This setup is a good candidate to test CORS. I’m going to run this demo in Chrome and use its developer tools but FireFox and IE have similar tools as well.

Start both the web api and the simple html app.

When the HTML page with the “get rockbands” button loads, press F12 to open the Chrome developer tools. Select the Network tab and press the button. You should see that the request was refused:

Connection refused

The Origin header was included in the headers collection but as the request came from a different domain and the api didn’t include the necessary Access-Control-Allow-Origin header the connection was refused:

Origin header

Let’s fix this.

Back in the API project right click References, Manage NuGet Packages. Make a search for “cors” and install the following package:

Web API Cors package

Next open the WebApiConfig.cs file and add the following before ‘config.MapHttpAttributeRoutes();’:

var cors = new EnableCorsAttribute("http://localhost:51737", "*", "GET");
config.EnableCors(cors);

This means that we want to enable CORS from http://localhost:51737 – which is the URL of the Html app – with any headers and only GET methods.

You can apply this attribute on a controller level like this:

[EnableCors("http://localhost:51737", "*", "GET")]
public class RockbandsController : ApiController

If you want to allow all origins then put a ‘ “*” ‘ as the first parameter in the constructor. You can allow multiple HTTP verbs by separating them with a comma: “GET,POST,PUT”.

Re-run the API and… …I don’t know about you but I got an exception:

System web exception

It’s great when an update from NuGet messes up other dependencies… I entered the following redirect in web.config and it worked again:

<dependentAssembly>
        <assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-5.0.0.0" newVersion="5.1.0.0" />
</dependentAssembly>

The version of System.Web.Http in my project was 5.1 instead of 5.0. This entry redirects all calls to 5.1. I in fact had to do the same for the System.Net.Http.Formatting package:

<dependentAssembly>
        <assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-5.0.0.0" newVersion="5.1.0.0" />
</dependentAssembly>

Run the web API app and navigate to /api/rockbands/ just to make sure you get the correct XML.

Refresh the CorsDemo HTML page, leave the developer tools open and press the Get rockbands button. It should go through:

CORS ok

Locate the response header and you’ll see that it includes what we needed:

Header OK

Read the last post of this series here.

View the list of MVC and Web API related posts here.

Advertisements

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

12 Responses to Introduction to .NET Web API 2 with C# Part 2

  1. Thank you for sharing this.

  2. Israel Pereira says:

    Thanks Andras ! All tutorials are very helpful !

  3. Alex Y says:

    Thank you Man. I really enjoy your blog.

    • Andras Nemes says:

      Hi Alex, thanks for your kind words.
      //Andras

      • Alex Y says:

        Hi Andras,

        I know you are not big fun of JavaScript 🙂 but would be nice to get you blog about the SPA (single page app architecture) with .net web api (EF for sql db and domain/model maybe built on DDD concept you blogged before) and Knockout.js with authentication. To see how it all fit together. Thanks a lot

      • Andras Nemes says:

        Hi Alex,

        Thanks for your suggestion but don’t hold your breath. I’m planning to blog more about EF and DDD in 2015 but SPA and JS are not on my radar. I’m currently working on a long-term series covering some components in Amazon Web Services to handle Big Data.

        //Andras

  4. mattwest2342 says:

    This has been a really helpful series so far, thanks for posting it! It’s nice to see a tutorial that just focuses on the subject it is trying to teach, rather than throwing in a half-dozen unrelated technologies. That usually just ends up making the tutorial very confusing, especially if the reader doesn’t know some of the additional technologies.

    This series, however, keeps it simple. That’s great! Very well done.

  5. RR says:

    What was the full reason for why you had to do Binding Redirect to avoid the exception. Is it because EnableCors Nuget package was compiled with an older version of System.Web.Http and you did not have the older version in your project ?

    Also, what if the EnableCors did not work (for whatever reason) with the binding redirect version of System.Web.Http. Wouldn’t it be safer to have both the versions of System.Web.Http referenced in your project ? Would that be possible or would it cause name conflicts ?

    Thank you.

    • Andras Nemes says:

      I had to add bindingRedirect since I had a higher version of system.web.http. I don’t think it’s possible to have two libraries with different versions in the same project. Which one would the project use? //Andras

  6. Duncan Wilson says:

    Hi Andras,
    Thank you for creating these articles and tutorials, I’ve been using them and learning a lot.

    Do you know how can I do a search of a Rockband called Smith&Son
    If I implement this route:
    Route(“rockbands/{name}”]

    and try to call it on Fiddler I will get:
    A potentially dangerous Request.Path value was detected from the client (&amp)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

ultimatemindsettoday

A great WordPress.com site

Elliot Balynn's Blog

A directory of wonderful thoughts

Softwarearchitektur in der Praxis

Wissenswertes zu Webentwicklung, Domain-Driven Design und Microservices

Technology Talks

on Microsoft technologies, Web, Android and others

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT BEST PRACTICES WITH MICROSOFT STACK & ANGULAR

Cyber Matters

Bite-size insight on Cyber Security for the not too technical.

Guru N Guns's

OneSolution To dOTnET.

Johnny Zraiby

Measuring programming progress by lines of code is like measuring aircraft building progress by weight.

%d bloggers like this: