5 Days of Simple.Web: Content Negotiation

Content negotiation

I’ve mentioned one of the key ingredients of a ReST API is “representation”. It is likely the representation sent back to the client is an abstract of the actual resource exposed. Furthermore the content of the representation will need to be encoded and in a format the client can consume, it will need to be negotiated.

Negotiate

Fortunately HTTP/1.1 provides us with headers to enable the negotiation between client and server.

Request
1
2
3
GET /customers HTTP/1.1
Accept: text/html
Accept-Charset: utf-8

When receiving a request Simple.Web will invoke the appropriate media-type handler for that requested.

Handle

Simple.Web supports content negotiation using available media-type handlers. The following media-type handlers are available as NuGet packages;

Media type handlers are discovered by identifying class-level attribute MediaTypes which also denote it’s supported MIME type(s). You are therefore able to add your own handlers with ease.

Example media-type handler
1
2
3
4
[MediaTypes(MediaType.Html, MediaType.XHtml)]
public class RazorHtmlMediaTypeHandler : IMediaTypeHandler
{
}

Respond

Response
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
<html>
  ...
</html>

If no matching handler is available Simple.Web will respond 415 Unsupported media type.

Razor

Simple.Web.Razor allows association of a view with a model, a handler, or both. Properties of any associated model or handler are exposed to the view, avoiding the need for an ambiguous view-bag. Should you wish to share data between layouts and sections without polluting your model or handler then ViewBag is available.

[GET] GetEndpoint.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace ContentNegotiation.Customers
{
  using System;

  using Simple.Web;
  using Simple.Web.Behaviors;

  [UriTemplate("/customers")]
  public class GetEndpoint : IGet, IOutput<IEnumerable<CustomersModel>>
  {
    private ICustomersQuery customerQuery;

    public GetEndpoint(ICustomersQuery customersQuery)
    {
      this.queryCustomers = queryCustomers;
    }

    public string Title { get; set; }

    public IEnumerable<CustomersModel> Customers { get; set; }

    public Status Get()
    {
      this.Title = "Customer list as of " + DateTime.Now.ToLongDateString();
      this.Customers = this.customersQuery.Execute();

      return 200;
    }
  }
}
Customers.cshtml (model)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@model IEnumerable<ContentNegotiation.Customers.CustomersModel>

<!DOCTYPE html>
<html>
  <body>
    <table>
      <thead>
        <th>
          <td>Forename</td>
          <td>Surname</td>
        </th>
      </thead>
      <tbody>
        @foreach (var customer in @Model) {
          <tr>
            <td>@customer.Forename</td>
            <td>@customer.Surname</td>
          </tr>
        }
      </tobdy>
    </table>
  </body>
</html>
Customers.cshtml (handler)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@handler ContentNegotiation.Customers.GetEndpoint

<!DOCTYPE html>
<html>
  <body>
    <h1>Welcome to @Handler.Title</h1>
    <table>
      <thead>
        <th>
          <td>Forename</td>
          <td>Surname</td>
        </th>
      </thead>
      <tbody>
        @foreach (var customer in @Handler.Customers) {
        <tr>
          <td>@customer.Forename</td>
          <td>@customer.Surname</td>
        </tr>
        }
      </tobdy>
    </table>
  </body>
</html>
Customers.cshtml (handler & model)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@handler ContentNegotiation.Customer.GetEndpoint
@model IEnumerable<ContentNegotiation.Customers.CustomersModel>

<!DOCTYPE html>
<html>
  <body>
    <h1>Welcome to @Handler.Title</h1>
    <table>
      <thead>
        <th>
          <td>Forename</td>
          <td>Surname</td>
        </th>
      </thead>
      <tbody>
        @foreach (var customer in @Model) {
        <tr>
          <td>@customer.Forename</td>
          <td>@customer.Surname</td>
        </tr>
        }
      </tobdy>
    </table>
  </body>
</html>

Discovery

I mentioned above that media-type handlers were “discovered” at runtime; to be exact they are imported from the AppDomain. To ensure the Type’s of media-type handlers in referenced assemblies are included I recommend a code reference that CodeAnalysis won’t think better for you.

You can do this using an IoC container (of which Simple.Web supports Autofac, Ninject, and StructureMap) to scan the appropriate assemblies, alternatively reference the assemblies at compile-time. For example;

Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace ContentNegotiation
{
  using System;

  internal class MainClass
  {
    public static Type[] EnforceReferencesFor = {
      typeof(Simple.Web.JsonNet.JsonMediaTypeHandler),
      typeof(Simple.Web.Xml.XmlMediaTypeHandler)
    };

    private static void Main (string[] args)
    {
      new Simple.Web.Hosting.Self.OwinSelfHost().Run();
    }
  }
}

This also has the added benefit that if you bootstrap the self-hosting from another assembly (e.g. acceptance tests) no direct references to Simple.Web or it’s dependencies will be required.

Sidenote If you know a better way round this nasty smell I’d love to hear it!

That’s all for now folks! :–)

Comments