Using Razor engine together with Asp.Net Web API to create a Hypermedia API

This blog post is created just for fun, and it's will be about how we can use Razor to create a Hypermedia API using XML as a hypermedia for a "Maze game" inspired from the book "Building Hypermedia APIs with HTML5 and Node", Mike Amundsen. Only the server-side API is covered in this blog post, so no Client.

Note: The code in this blog post will use the WebAPI Contrib's Formatting.RazorViewEngine

Here is a short description of the game:

In this "Maze Game", a client should be able to request a maze to play. The client should be able to navigate from a starting point through the maze to the exit. The cells in the maze can have different exit (doorways) into other cells (north, south, east and west).

To design the media type for the game, we first need to know what clients can do, so here is a list of requirements of the game:

  • A list of available mazes to select among.
  • Be able to select one maze to play.
  • See all the doorways in each cell.
  • Navigate through a selected doorway into the next cell.
  • See the exit of the maze and navigate through the exit.

To design the hypermedia we can take the requirements and turn it into some states.

The following is a list of application states and transitions for each identified state of the maze:

Collection State

    The response represents a list of mazes.

Possible transitions are:

1) Select a maze (Item State), or
2) reload the list (Collection state).

Item State

    The response represents a single maze.

    Possible transition are:

  1. Start into the maze, or
  2. reload the list (Collection Sate), or
  3. reload the maze (Item State).

Cell State

    The response represents a single cell in the maze.

    Possible transitions are:

  1. Continue through one of the doorways to the next cell (Cell State)
  2. Return to the maze (Item State)
  3. Return to the maze list (Collection State), or
  4. reload the cell (Cell State).

Error State

    The response represents the details of an error. No transitions from this state.

Here is the hypermedia design of the above states:

<maze>
    <collection/>
    <item/>
    <cell/>
    <error>
</maze>


For the Item State's transition the <link href="…" rel="maze"/> will be used, the "href" and "rel" attribute is used to hold the URI and transition identifier.

For the Collection State the "href" attribute will be used for reloading the list:

<collection href="…">

A fully Collection State could look like this

<maze>
    <collection href="…">
        <link href="…" rel="maze" />
        <link href="…" rel="maze" />
        …
    </collection>
</maze>

The Item State is represented by using the <item/> element, it also uses the <link/> element for transitions and the" href" attribute for reloading the maze details. Here is an example:

<maze>
    <item href="…">
        <link href="…" rel="collection" />
        <link href="…" rel="start" />
        …
    </item>
</maze>


The Cell State is represented in the following way:

<maze>
    <cell href="…">
        <link href="…" rel="north" />
        <link href="…" rel="south" />
        <link href="…" rel="east" />
        <link href="…" rel="west" />
        <link href="…" rel="exit" />
        <link href="…" rel="maze" />
        <link href="…" rel="collection" />
    </cell>
</maze>


Now when the design of the hypermedia is done, we can start creating our API and the Razor views that should render the media.

The following code are simple fakes that representation the Model of the Maze:

using System.Collections.Generic;
using System.Linq;

namespace MvcWebApiSiteTest.Models
{
public class MazeDb
{
public MazeGame GetMaze()
{
var maze = new Maze { Id = "my-only-maze", Description = "My only maze" };

            maze.AddCell(new Cell { Id = 1, Doorways = new[] { new DoorWay { Id = 1, Direction = "north", Cell = 2 } } });
maze.AddCell(new Cell { Id = 2, Doorways = new[] { new DoorWay { Id = 1, Direction = "exit" } } });

            return new MazeGame { Mazes = new List<Maze> { maze } };

        }
}

   public class MazeGame
{
public IEnumerable<Maze> Mazes { get; set; }
}

public class Maze
{
private IList<Cell> _cells = new List<Cell>();

public string Id { get; set; }
public string Description { get; set; }
public Cell Start { get { return Cells.First(); }}
public IEnumerable<Cell> Cells { get { return _cells; }}

public void AddCell(Cell cell)
{
cell.Maze = this;
_cells.Add(cell);
}
}

public class Cell
{
public Maze Maze { get; set; }
public int Id { get; set; }
public IEnumerable<DoorWay> Doorways { get; set; }
}

public class DoorWay
{
public int Id { get; set; }
public string Direction { get; set; }
public int Cell { get; set; }
}
}

 

The following is the ApiController used. The code for accessing the Maze are just fakes (Because the blog post is not about how to write the server-side code, it's about how to use the RazorViewEngine to create a hypermedia, the code is simple and stupid and far from perfect!):

using System.Web.Http;

namespace MvcWebApiSiteTest.Controllers
{
using System.Linq;
using MvcWebApiSiteTest.Models;

public class MazeController : ApiController
{
// GET /maze
public MazeGame Get()
{
var mazeDb = new MazeDb();
return mazeDb.GetMaze();
}

// GET /maze/{maze}
public Maze Get(string maze)
{
var mazeDb = new MazeDb();
return mazeDb.GetMaze()
.Mazes.Single(m => m.Id == maze);
}

// GET /maze/{maze}/{id}
public Cell Get(string maze, int id)
{
var mazeDb = new MazeDb();
return mazeDb.GetMaze()
.Mazes.Single(m => m.Id == maze)
.Cells.Single(c => c.Id == id);
}
}
}



The following code configures the route for the API so it can take "maze" and "id" from the URL as parameters (instead of using Querystrings) into the Get methods of the MazeController:


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


Now when the server-side API is created, we can go on to the Razor Views that will render the hypermedia designed earlier in this blog post.

The WebApi Contrib's Formatting RazorViewEngine can use convention before configuration to locate the View for rendering the Model the MazeController returns. The name of the view should be the same name as the class the MazeController returns. The Views are created in the Views folder and the views are MazeGame.cshtml (Representation the Collection State of the Maze), Maze.cshtml (Represents the Item State) and the Cell.cshtml (Represents the Cell State). Here is the code of the Views:

MazeGame.cshtml


<?xml version="1.0" encoding="utf-8" ?>
<maze>
<collection href="http://weblogs.asp.net/mazeGame">
@foreach (var maze in Model.Mazes)
{
<link href="http://weblogs.asp.net/maze/@maze.Id" rel="maze" />
}
</collection>
</maze>


Here is a representation of the MazeGame.cshtml ouput:

<?xml version="1.0" encoding="utf-8" ?> 

<maze>

    <collection href="http://weblogs.asp.net/mazeGame">

            <link href="http://weblogs.asp.net/maze/my-only-maze" rel="maze" />

    </collection>

</maze>



Maze.cshtml


<?xml version="1.0" encoding="utf-8" ?>
<maze>
<item href="http://weblogs.asp.net/maze/@Model.Id" >
<link href="http://weblogs.asp.net/mazeGame" rel="collection">
<link href="http://weblogs.asp.net/maze/@Model.Id/@Model.Start.Id" rel="start" />
</item>
</maze>

 

Here is a representation of the Maze.cshtml output:


<?xml version="1.0" encoding="utf-8" ?>

<maze>

    <item href="http://weblogs.asp.net/maze/my-only-maze" >

        <link href="http://weblogs.asp.net/mazeGame" rel="collection">

        <link href="http://weblogs.asp.net/maze/my-only-maze/1" rel="start" />

    </item>

</maze>



Cell.cshtml

 

<?xml version="1.0" encoding="utf-8" ?> 

<maze>

    <cell href="http://weblogs.asp.net/maze/@Model.Maze.Id/@Model.Id">

        @foreach (var cell in Model.Doorways)

        {

            <link href="http://weblogs.asp.net/maze/@Model.Maze.Id/@cell.Cell" rel="@cell.Direction" />

        }

        <link href="http://weblogs.asp.net/maze/@Model.Maze.Id" rel="maze" />

        <link href="http://weblogs.asp.net/maze" rel="collection" />

    </cell>

</maze>

 

Here is a representation of the Cell.cshtml output:

<?xml version="1.0" encoding="utf-8" ?> 

<maze>

    <cell href="http://weblogs.asp.net/maze/my-only-maze/1">

        <link href="http://weblogs.asp.net/maze/my-only-maze/2" rel="north" />

        <link href="http://weblogs.asp.net/maze/my-only-maze" rel="maze" />

        <link href="http://weblogs.asp.net/maze" rel="collection" />

    </cell>

</maze>

 

Now when the Views for rending the hypermedia is done, we need to configure so we can use a specific media type to access our hypermedia. We will use "application/maze+xml" as the media type.

To add the support of our own media type "application/maze+xml" we can take advantage of the WebApiContrib Razor Formatter's HtmlMediaTypeViewFormatter. We can simply add the "application/maze+xml" media type to the HtmlMediaTypeViewFormatter, here is the Application_Start method of the Global.asax to add our media type and use the HtmlMediaTypeViewFormatter:

protected void Application_Start()

{

      AreaRegistration.RegisterAllAreas();

 

      var formatter = new HtmlMediaTypeViewFormatter();

      formatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/maze+xml"));

 

      GlobalConfiguration.Configuration.Formatters.Add(formatter);

 

      GlobalViews.DefaultViewParser = new RazorViewParser();

      GlobalViews.DefaultViewLocator = new RazorViewLocator();

 

      WebApiConfig.Register(GlobalConfiguration.Configuration);

      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

      RouteConfig.RegisterRoutes(RouteTable.Routes);

      BundleConfig.RegisterBundles(BundleTable.Bundles);

}


To access the Maze Hypermedia API by a HTTP GET, make sure the HTTP Header Accept is set to "application/maze+xml":

Accept: application/maze+xml


All done!

By using ASP.Net Web API and the WebApiContrib.Formatting.RazorViewEngine, we can use Razor to create a hypermedia, wasn't that cool? ;)

By following me on twitter @fredrikn, you will know when I publish new blog posts.

 

 

 

 

 

 

Published Sunday, January 20, 2013 9:38 PM by Fredrik N

Comments

No Comments

Leave a Comment

(required) 
(required) 
(optional)
(required)