ASP.Net Web API and using Razor the next step
In my previous blog post “Using Razor together with ASP.NET Web API” I wrote about a solution to use a MediaTypeFormatter to render HTML by using Razor when the API is accessed from a browser. I’m now sort of done with the basics and will share the current solution in this blog post. The source code will later be available.
I decided to make the solution more extendable so I created a HtmlMediaTypeViewFormatter that inherits from the MediaTypeFormatter:
public class HtmlMediaTypeViewFormatter : MediaTypeFormatter { //... }
With the new generic solution any kind of “parser” can be used to render the HTML, not only by using Razor. I made this possible by using a IViewParser:
public interface IViewParser { byte[] ParseView(IView view, string viewTemplate, Encoding encoding); }
The IViewParser’s responsibility is to implement the logic to use a template (for example a razor template) and parse a view’s model into a encoded byte array.
Another interface that is introduce is the IViewLocator, with the IViewLocator it will be easy to replace how to locate the templates passed into the IViewParser. At the moment the IViewLocator also has the responsibility to return the content of the located template, so the default RazorViewLocator in the project will locate and read a .cshtml or .vbhtml file and the content of the file is passed as an argument to the IViewParser. Here is part of the IViewLocator code:
public interface IViewLocator { string GetView(string siteRootPath, IView view); }
internal class RazorViewLocator : IViewLocator { private readonly string[] viewLocationFormats = new[] { "~\\Views\\{0}.cshtml", "~\\Views\\{0}.vbhtml", "~\\Views\\Shared\\{0}.cshtml", "~\\Views\\Shared\\{0}.vbhtml" }; public string GetView(string siteRootPath, IView view) { if (view == null) throw new ArgumentNullException("view"); var path = GetPhysicalSiteRootPath(siteRootPath); foreach(string viewLocationFormat in viewLocationFormats) { var potentialViewPathFormat = viewLocationFormat.Replace("~", GetPhysicalSiteRootPath(siteRootPath)); var viewPath = string.Format(potentialViewPathFormat, view.ViewName); if (File.Exists(viewPath)) return File.ReadAllText(viewPath); } throw new FileNotFoundException(string.Format("Can't find a view with the name '{0}.cshtml' or '{0}.vbhtml in the '\\Views' folder under path '{1}'", view.ViewName, path)); } ///... }
Here is some code from the HtmlMediaTypeViewFormatter, so you can get a glimpse how the IViewLocator and IViewParser is used:
public override Task WriteToStreamAsync( Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext) { return TaskHelpers.RunSync(() => { var encoding = SelectCharacterEncoding(contentHeaders); var parsedView = ParseView(type, value, encoding); stream.Write(parsedView, 0, parsedView.Length); stream.Flush(); }); } private byte[] ParseView(Type type, object model, System.Text.Encoding encoding) { var view = model as IView; if (view == null) view = new View(GetViewName(model), model, type); var viewTemplate = _viewLocator.GetView(_siteRootPath, view); return _viewParser.ParseView(view, viewTemplate, encoding); }
Note: The WriteToStreamAsync in the RTM code of ASP.NET WebAPI is different from the above code. I decided to still use the bits shipped with Visual Studio 2012 RC. This will of course be changed later.
The IViewLocator and IViewParser can be changed by using a global configuration class, GlobalViews, so in App_Start of a Web API project or in Global.asax the locator and parser can be replaced with another implementation, for example:
GlobalViews.DefaultViewLocator = new MyViewParser(); GlobalViews.DefaultViewLocator = new DatabaseViewLocator();
The HtmlMediaTypeViewFormatter can also inject the dependencies to a locator and parser.
How to decide which view to be used
There are different ways to configure which View that should be used when accessing a API of a ApiController. Here are the examples:
Convention
public class CustomerController : ApiController { // GET api/customer public Customer Get() { return new Customer { Name = "John Doe", Country = "Sweden" }; } }
By default with no configurations at all (just adding the HtmlMediaTypeViewFormatter to the Formatters collection in the Global.asax) a view will be located by using the name of the returned model, in the above code “Customer”
Configuration
By using annotation (with the ViewAttribute) you can specify which view a specific model should use:
[View("Customer")] public class Customer { public string Name { get; set; } public string Country { get; set; } }
By using the GlobalViews in the Global.asax, mapping between model an views can be configured, here is code from the App_Start folder:
public class ViewConfig { public static void RegisterViews(IDictionary<Type, string> views) { views.Add(typeof(Customer), "CustomerViaConfig"); } }
So if the Customer type is returned from the Web API, the “CustomerViaConfig” view will be used.
By returning an IView class, the view can be specified within the Web API’s return result:
public class CustomerController : ApiController { // GET api/customer public View Get() { return new View("CustomerViaView", new Customer { Name = "John Doe", Country = "Sweden" }); } }
Summary
By using a HtmlMediaTypeViewFormatter, we can now render HTML when accessing our Web API from a browser, and we can specify which view to be used by using different kind of configuration or convention. It’s easy to replace the parser that is used to render the HTML and also how and where to locate a view to be used. The source code will sometime in a near future (I hope) be available for download.
My next goal is to make a WHOLE web site explaining the use of my project, just by using ASP.Net Web API.. I promise to let all of you know when that happens.
If you want to know when I post a new blog post, you can simply follow me on twitter @fredrikn