Language

Create a REST API with Attribute Routing in Web API 2

By |

This tutorial uses pre-release features.

Web API 2 supports a new type of routing, called attribute routing. In this tutorial, you will use attribute routing to create a REST API for a collection of books. The API will support the following action:

ActionExample URI
Get a list of all books./api/books
Get a book by ID./api/books/1
Get the details of a book./api/books/1/details
Get a list of books by genre./api/books/fantasy
Get a list of books by publication date./api/books/date/2013-02-16
/api/books/date/2013/02/16 (alternate form)
Get a list of books by a particular author./api/authors/1/books

All methods are read-only (HTTP GET requests).

For the data layer, we'll use Entity Framework. Book records will have the following fields:

  • ID
  • Title
  • Genre
  • Publication date
  • Price
  • Description
  • AuthorID (foreign key to an Authors table)

For most requests, however, the API will return a subset of this data (title, author, and genre). To get the complete record, the client requests /api/books/{id}/details.

Create the Visual Studio Project

Start by running Visual Studio Express 2013 Preview for Web or Visual Studio 2013 Preview. From the File menu, select New and then select Project.

In the Templates pane, select Installed Templates and expand the Visual C# node. Under Visual C#, select Web. In the list of project templates, select ASP.NET MVC 4 Web Application. Name the project "BooksAPI".

In the New ASP.NET Project dialog, select the Empty template. Under “Add folders and core references for”, select the Web API checkbox. Click Create Project.

This creates a skeleton project that is configured for Web API functionality.

Domain Models

Next, add classes for domain models. In Solution Explorer, right-click the Models folder. Select Add, then select Class. Name the class Author.

Replace the code in Author.cs with the following:

using System.ComponentModel.DataAnnotations;

namespace BooksAPI.Models
{
    public class Author
    {
        public int AuthorId { get; set; }
        [Required]
        public string Name { get; set; }
    }
}

Now add another class named Book.

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BooksAPI.Models
{
    public class Book
    {
        public int BookId { get; set; }
        [Required]
        public string Title { get; set; }
        public string Genre { get; set; }
        public DateTime PublishDate { get; set; }
        public string Description { get; set; }
        public int AuthorId { get; set; }
        [ForeignKey("AuthorId")]
        public Author Author { get; set; }
    }
}

Add a Web API Controller

In this step, we'll add a Web API controller that uses Entity Framework as the data layer.

Press F6 to build the project now. Entity Framework uses reflection to discover the properties of the models, so it requires a compiled assembly to create the database schema.

In Solution Explorer, right-click the Controllers folder. Select Add, then select Scaffold.

In the Add Scaffold dialog, select “Web API 2 Controller with read/write actions, using Entity Framework.” (You might need to scroll down to see this option.)

In the Add Controller dialog, for Controller name, enter "BooksController". For Model class, select "Book". (If you don’t see the Book class listed in the dropdown, make sure that you built the project.) In the Data context class dropdown, select “<New data context...>”.

In the New Data Context dialog, name the data context type “BooksContext”. Click OK.

Click Add.

The Add Scaffold wizard adds a class named BooksController that defines the API controller. It also adds a class named BooksContext in the Models folder, which defines the data context for Entity Framework.

In the BooksController class, go ahead and delete the PutBook, PostBook, and DeleteBook methods. These methods implement update, create, and delete functionality. For this tutorial, however, our API will be read-only.

Add a Database Initializer

Entity Framework lets you populate the database on startup, and automatically recreate the database whenever the models change. This feature is useful during development, because you always have some test data, even if you change the models.

In Solution Explorer, right-click the Models folder and create a new class named BooksContextInitializer. Paste in the following code:


public class BooksContextInitializer : DropCreateDatabaseIfModelChanges<BooksContext>
{
    protected override void Seed(BooksContext context)
    {
        var authors = new List<Author>() {
                new Author() { AuthorId = 1, Name = "Ralls, Kim" },
                new Author() { AuthorId = 2, Name = "Corets, Eva" },
                new Author() { AuthorId = 3, Name = "Randall, Cynthia" },
                new Author() { AuthorId = 4, Name = "Thurman, Paula" }
            };
        authors.ForEach(x => context.Authors.Add(x));
        context.SaveChanges();

        var books = new List<Book>() {
            new Book() { BookId = 1,  Title= "Midnight Rain", Genre = "Fantasy", 
                PublishDate = new DateTime(2000, 12, 16), AuthorId = 1, Description =
                "A former architect battles an evil sorceress.", Price = 14.95M }, 

            new Book() { BookId = 2, Title = "Maeve Ascendant", Genre = "Fantasy", 
                PublishDate = new DateTime(2000, 11, 17), AuthorId = 2, Description =
                "After the collapse of a nanotechnology society, the young" +
                "survivors lay the foundation for a new society.", Price = 12.95M },

            new Book() { BookId = 3, Title = "The Sundered Grail", Genre = "Fantasy", 
                PublishDate = new DateTime(2001, 09, 10), AuthorId = 2, Description =
                "The two daughters of Maeve battle for control of England.", Price = 12.95M },

            new Book() { BookId = 4, Title = "Lover Birds", Genre = "Romance", 
                PublishDate = new DateTime(2000, 09, 02), AuthorId = 3, Description =
                "When Carla meets Paul at an ornithology conference, tempers fly.", Price = 7.99M },

            new Book() { BookId = 5, Title = "Splish Splash", Genre = "Romance", 
                PublishDate = new DateTime(2000, 11, 02), AuthorId = 4, Description =
                "A deep sea diver finds true love 20,000 leagues beneath the sea.", Price = 6.99M},
            };
        books.ForEach(x => context.Books.Add(x));
        context.SaveChanges();
    }
}

In the BooksContext class, add the following static constructor.

static BooksContext()
{
    Database.SetInitializer(new BooksContextInitializer());
}

Add DTO Classes

If you run the application now and send a GET request to /api/books/1, the response will look something like this:

{
    "BookId":1,
    "Title":"Midnight Rain",
    "Genre":"Fantasy",
    "PublishDate":"2000-12-16T00:00:00",
    "Description":"A former architect battles an evil sorceress.",
    "AuthorId":1,
    "Author":{
         "AuthorId":1,
         "Name":"Ralls, Kim"
     }
 }

(I added the indentation to make it more readable.)

However, our design calls for most of the API calls to return a subset of the data. Also, having the author returned in a separate sub-object is somewhat inconvenient.

The answer is to return a data transfer object (DTO) instead of the EF model. A DTO is an object that is designed only to carry data. It does not define any business logic. We'll use DTOs to send the data in the desired format.

In Solution Explorer, right-click the project and select Add | Folder. Name the folder "DTOs".

Next, add a class named BookDto to the DTOs folder.

namespace BooksAPI.DTOs
{
    public class BookDto
    {
        public string Title { get; set; }
        public string Author { get; set; }
        public string Genre { get; set; }
    }
}

Then add another class named BookDetailDto.

using System;

namespace BooksAPI.DTOs
{
    public class BookDetailDto
    {
        public string Title { get; set; }
        public string Genre { get; set; }
        public DateTime PublishDate { get; set; }
        public string Description { get; set; }
        public Decimal Price { get; set; }
        public string Author { get; set; }
    }
}

Update the BooksController class to return BookDto instances. We'll use the Queryable.Select method to project Book instances to BookDto instances. Here is the updated code for the controller class.

using BooksAPI.DTOs;
using BooksAPI.Models;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace BooksAPI.Controllers
{
    public class BooksController : ApiController
    {
        private BooksContext db = new BooksContext();

        // Typed lambda expression for Select() method. 
        private static readonly Expression<Func<Book, BookDto>> AsBookDto =
            x => new BookDto
            {
                Title = x.Title,
                Author = x.Author.Name,
                Genre = x.Genre
            };

        public IEnumerable<BookDto> GetBooks()
        {
            return db.Books.Include(b => b.Author).Select(AsBookDto);
        }

        public BookDto GetBook(int id)
        {
            BookDto book = db.Books.Include(b => b.Author)
                .Where(b => b.AuthorId == id).Select(AsBookDto).FirstOrDefault();
            if (book == null)
            {
                throw new HttpResponseException(
                    Request.CreateResponse(HttpStatusCode.NotFound));
            }
            return book;
        }

        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

Enable Attribute Routing

Open the file WebApiConfig.cs, which is located in the App_Start folder. Add the following code to the WebApiConfig.Register method:

public static void Register(HttpConfiguration config)
{
    // New code
    config.MapHttpAttributeRoutes();

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

The MapHttpAttributeRoutes method is an extension method defined in the System.Web.Http.HttpConfigurationExtensions class.

Get Book Details

To get book details, the client will send a GET request to /api/books/id/details, where id is the ID of the book.

Add the following method to the BooksController class.

[HttpGet("api/books/{id}/details")]
public BookDetailDto GetBookDetail(int id)
{
    var book = (from b in db.Books.Include(b => b.Author)
                where b.AuthorId == id
                select new BookDetailDto
                {
                    Title = b.Title,
                    Genre = b.Genre,
                    PublishDate = b.PublishDate,
                    Price = b.Price,
                    Description = b.Description,
                    Author = b.Author.Name
                }).FirstOrDefault();

    if (book == null)
    {
        throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
    }
    return book;
}

Notice the [HttpGet] attribute on this method. This attribute defines a route for the method. The string "api/books/{id}/details" is the URI template for the route. Any request that matches this template will be routed to the method. As you can see, attribute routing lets you define routes that are tailored to specific actions.

Get Books By Genre

To get a list of books in a specific genre, the client will send a GET request to /api/books/genre, where genre is the name of the genre. (For example, /get/books/fantasy.)

Add the following method to BooksController.

[HttpGet("api/books/{genre}")]
public IEnumerable<BookDto> GetBooksByGenre(string genre)
{
    return db.Books.Include(b => b.Author)
        .Where(b => b.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase))
        .Select(AsBookDto);
}

Here we are defining a route that contains a {genre} parameter in the URI template. However, this route conflicts with our existing GetBook method. As it stands, the routing engine will not be able to distinguish these two URIs:

/api/books/1

/api/books/fantasy

To fix this problem, we'll add a constraint to the GetBook route, so that only integer values will match the {id} parameter. To specify this constraint, add a routing attribute to the GetBook method:

[HttpGet("api/books/{id:int}")]  // new code
public BookDto GetBook(int id)
{
    // ... implementation does not change
}

The {id:int} parameter specifies a constraint (the "int" in the parameter). This parameter will only match URI segments that can be converted into integer values. Thus, /api/books/1 is a match, but /api/books/fantasy is not.

Get Books By Author

To get a list of a books for a particular author, the client will send a GET request to /api/authors/id/books, where id is the ID of the author.

Add the following method to BooksController.

[HttpGet("api/authors/{authorId}/books")]
public IEnumerable<BookDto> GetBooksByAuthor(int authorId)
{
    return db.Books.Include(b => b.Author)
        .Where(b => b.AuthorId == authorId)
        .Select(AsBookDto);
}

This example is interesting because "books" is being treated as a child resource of "authors". This pattern is quite common in RESTful APIs.

Get Books By Publication Date

To get a list of books by publication date, the client will send a GET request to /api/books/date/yyyy-mm-dd, where yyyy-mm-dd is the date.

Here is a first attempt:

[HttpGet("api/books/date/{pubdate:datetime}")]
public IEnumerable<BookDto> GetBooks(DateTime pubdate)
{
    return db.Books.Include(b => b.Author)
        .Where(b => DbFunctions.TruncateTime(b.PublishDate) 
            == DbFunctions.TruncateTime(pubdate))
        .Select(AsBookDto);
}

The {pubdate:datetime} parameter is constrained to match a DateTime value. This works, but it's actually more permissive than we'd like. For example, these URIs will also match the route:

/api/books/date/Thu, 01 May 2008

/api/books/date/2000-12-16T00:00:00

There's nothing wrong with allowing these URIs, but if you prefer, you can further restrict the route by adding a regular-expression constraint.

[HttpGet("api/books/date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
public IEnumerable<BookDto> GetBooks(DateTime pubdate)
{
    // ...
}

Now only dates in the form "yyyy-mm-dd" will match. Notice that we don't use the regex to validate that we got a real date. That is handled when Web API tries to convert the URI segment into a DateTime instance. An invalid date such as '2012-47-99' will fail to be converted, and the client will get a 404 error.

We can also support URIs of the form /api/books/date/yyyy/mm/dd by adding another [HttpGet] attribute with a different regex.

[HttpGet("api/books/date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
[HttpGet("api/books/date/{*pubdate:datetime:regex(\\d{4}/\\d{2}/\\d{2})}")]  // new
public IEnumerable<BookDto> GetBooks(DateTime pubdate)
{
    // ...
}

There is a subtle but important detail here. The second template has a wildcard character (*) at the start of the {pubdate} parameter:

{*pubdate: ... }

This tells the routing engine to match the rest of the URI. By default, a template parameter matches a single URI segment. But in this case, we want the {pubdate} parameter to span several URI segments:

/api/books/date/2013/06/17

Route Prefixes

Here is our controller so far (without the method bodies).

public class BooksController : ApiController
{
    [HttpGet("api/books")]
    public IEnumerable<BookDto> GetBooks()

    [HttpGet("api/books/{id:int}")]
    public BookDto GetBook(int id)

    [HttpGet("api/books/{id}/details")]
    public BookDetailDto GetBookDetail(int id)

    [HttpGet("api/books/{genre}")]
    public IEnumerable<BookDto> GetBooksByGenre(string genre)

    [HttpGet("api/authors/{authorId}/books")]
    public IEnumerable<BookDto> GetBooksByAuthor(int authorId)

    [HttpGet("api/books/date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
    [HttpGet("api/books/date/{*pubdate:datetime:regex(\\d{4}/\\d{2}/\\d{2})}")]
    public IEnumerable<BookDto> GetBooks(DateTime pubdate)
}

For consistency, I added an [HttpGet] routing attribute to the GetBooks method, so all the methods use attribute routing.

You can see that the “api” URI segment is repeated on all of the routes. To avoid repeating it everywhere, you can specify a prefix for the entire controller by using the [RoutePrefix] attribute.

[RoutePrefix("api")]
public class BooksController : ApiController
{
    [HttpGet("books")]
    public IEnumerable<BookDto> GetBooks()

    [HttpGet("books/{id:int}")]
    public BookDto GetBook(int id)

    [HttpGet("books/{id}/details")]
    public BookDetailDto GetBookDetail(int id)

    [HttpGet("books/{genre}")]
    public IEnumerable<BookDto> GetBooksByGenre(string genre)

    [HttpGet("authors/{authorId}/books")]
    public IEnumerable<BookDto> GetBooksByAuthor(int authorId)

    [HttpGet("books/date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
    [HttpGet("books/date/{*pubdate:datetime:regex(\\d{4}/\\d{2}/\\d{2})}")]
    public IEnumerable<BookDto> GetBooks(DateTime pubdate)
}

You can put more than one [RoutePrefix] attribute on a controller. For example:

[RoutePrefix("api")]
[RoutePrefix("api/v1")]
public class BooksController : ApiController
{
    [HttpGet("books")]
    public IEnumerable<BookDto> GetBooks()

    [HttpGet("books/{id:int}")]
    public BookDto GetBook(int id) 

    // Other methods ...
}

Now these two URIs return the same resource:

/api/books/1

/api/v1/books/1

Summary

Attribute routing gives you more control and greater flexibility when designing the URIs for your API.

Mike Wasson

By Mike Wasson, Mike Wasson is a programmer-writer at Microsoft.

Table of Contents

Getting Started with ASP.NET Web API

Creating Web APIs

Web API Clients

Web API Routing and Actions

Working with HTTP

Formats and Model Binding

OData Support in ASP.NET Web API

Security

Hosting ASP.NET Web API

Testing and Debugging

Extensibility

Resources