Using Razor together with ASP.NET Web API

On the blog post “If Then, If Then, If Then, MVC” I found the following code example:

[HttpGet]
public ActionResult List() { var list = new[] { "John", "Pete", "Ben" }; if (Request.AcceptTypes.Contains("application/json")) { return Json(list, JsonRequestBehavior.AllowGet); } if (Request.IsAjaxRequest()) [ return PartialView("_List", list); } return View(list); }


The code is a ASP.NET MVC Controller where it reuse the same “business” code but returns JSON if the request require JSON, a partial view when the request is an AJAX request or a normal ASP.NET MVC View.

The above code may have several reasons to be changed, and also do several things, the code is not closed for modifications. To extend the code with a new way of presenting the model, the code need to be modified. So I started to think about how the above code could be rewritten so it will follow the Single Responsibility and open-close principle. I came up with the following result and with the use of ASP.NET Web API:

public String[] Get()
{
      return new[] { "John", "Pete", "Ben" };
}

 

It just returns the model, nothing more. The code will do one thing and it will do it well. But it will not solve the problem when it comes to return Views. If we use the ASP.NET Web Api we can get the result as JSON or XML, but not as a partial view or as a ASP.NET MVC view. Wouldn’t it be nice if we could do the following against the Get() method?

 

Accept: application/json

JSON will be returned – Already part of the Web API

 

Accept: text/html

Returns the model as HTML by using a View

 

The best thing, it’s possible!

 

By using the RazorEngine I created a custom MediaTypeFormatter (RazorFormatter, code at the end of this blog post) and associate it with the media type “text/html”. I decided to use convention before configuration to decide which Razor view should be used to render the model. To register the formatter I added the following code to Global.asax:

GlobalConfiguration.Configuration.Formatters.Add(new RazorFormatter());


Here is an example of a ApiController that just simply returns a model:

using System.Web.Http;

namespace WebApiRazor.Controllers
{
    public class CustomersController : ApiController
    {
        // GET api/values
        public Customer Get()
        {
            return new Customer { Name = "John Doe", Country = "Sweden" };
        }
    }


    public class Customer
    {
        public string Name { get; set; }

        public string Country { get; set; }
    }
}

 

Because I decided to use convention before configuration I only need to add a view with the same name as the model, Customer.cshtml, here is the example of the View:

 

<!DOCTYPE html>
<html>
    
    <head>
        <script src="http://ajax.aspnetcdn.com/ajax/jquery/jquery-1.5.1.min.js" type="text/javascript"></script>
    </head>

    <body>

        <div id="body">
            <section>
                
                <div>
                    <hgroup>
                        <h1>Welcome '@Model.Name' to ASP.NET Web API Razor Formatter!</h1>
                    </hgroup>
                </div>
                <p>
                    Using the same URL "api/values" but using AJAX: <button>Press to show content!</button>
                </p>
                <p>
                    
                </p>

            </section>
        </div>

    </body>
    
    <script type="text/javascript">

        $("button").click(function () {

            $.ajax({
                url: '/api/values',
                type: "GET",
                contentType: "application/json; charset=utf-8",
                success: function(data, status, xhr)
                {
                    alert(data.Name);
                },
                error: function(xhr, status, error)
                {
                    alert(error);
                }});
            });
</script>
</html>

 

Now when I open up a browser and enter the following URL: http://localhost/api/customers the above View will be displayed and it will render the model the ApiController returns. If I use Ajax against the same ApiController with the content type set to “json”, the ApiController will now return the model as JSON.

Here is a part of a really early prototype of the Razor formatter (The code is far from perfect, just use it for testing). I will rewrite the code and also make it possible to specify an attribute to the returned model, so it can decide which view to be used when the media type is “text/html”, but by default the formatter will use convention:

using System;
using System.Net.Http.Formatting;

namespace WebApiRazor.Models
{
    using System.IO;
    using System.Net;
    using System.Net.Http.Headers;
    using System.Reflection;
    using System.Threading.Tasks;

    using RazorEngine;

    public class RazorFormatter : MediaTypeFormatter
    {
        public RazorFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html")); 
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xhtml+xml"));
        }

        //...
public override Task WriteToStreamAsync( Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext) { var task = Task.Factory.StartNew(() => { var viewPath = // Get path to the view by the name of the type var template = File.ReadAllText(viewPath); Razor.Compile(template, type, type.Name); var razor = Razor.Run(type.Name, value); var buf = System.Text.Encoding.Default.GetBytes(razor); stream.Write(buf, 0, buf.Length); stream.Flush(); }); return task; } } }

 

Summary

By using formatters and the ASP.NET Web API we can easily just extend our code without doing any changes to our ApiControllers when we want to return a new format. This blog post just showed how we can extend the Web API to use Razor to format a returned model into HTML.

 

If you want to know when I will post more blog posts, please feel free to follow me on twitter:   @fredrikn

3 Comments

  • Cool. In your RazorFormatter class where you declare the viewPath variable, you're missing some code.


  • Adam: I just removed the code for locating the path to the .cshtml, it's not important for the concept of the blog post.

    Ben Foster: I know, but the reason I pointed to your blog was because the first code was a good base example. I couldn't just copy your code, so I thought it was better to point to your blog. I hope it was ok?

  • Fredrik, of course :)

Comments have been disabled for this content.