sebnilsson.com | Liquid Development Is What I Do
Seb Nilsson

API-Versioning in ASP.NET

The ASP.NET Route Versions-library was created after I was inspired by a discussion with a colleague and reading the great article Your API versioning is wrong by Troy Hunt, where he concludes that you don't need a war of preferences between different ways of versioning your API, you can actually support multiple ways in the same API.

In his article, Troy lists 3 ways (to do it wrong), which I have implemented for ASP.NET Core, and added support for one more way, which is URL versioning. This library supports the following ways to version your API:

  • URL versioning
  • Query string versioning
  • Custom request header
  • Content type

URL versioning:

HTTP GET:
https://my-web-app.com/api/v2/customers

Query string versioning:

HTTP GET:
https://my-web-app.com/api/customers?api-version=2

Custom request header:

HTTP GET:
https://my-web-app.com/api/customers
api-version: 2

Content type:

HTTP GET:
https://my-web-app.com/api/customers
Accept: application/vnd.api-version.v2+json

[RouteVersion]-attribute

All you need to do is use the [RouteVersion]-attribute on the Controller-Actions you want to version and provide the route-version as an argument:

[Route("api/v{api-version}/[controller]")]
[Route("api/[controller]")]
[ApiController]
public class CustomersController : ControllerBase
{
    [HttpGet]
    [RouteVersion(1)]
    public ActionResult<string> GetV1()
    {
        return "Get Customers Version 1";
    }

    [HttpGet]
    [RouteVersion(2)]
    public ActionResult<string> GetV2()
    {
        return "Get Customers Version 2";
    }

    [HttpPost]
    [RouteVersion(1)]
    public ActionResult<string> PostV1()
    {
        return "Post Customers Version 1";
    }

    [HttpPost]
    [RouteVersion(2)]
    public ActionResult<string> PostV2()
    {
        return "Post Customers Version 2";
    }
}

The attribute will only resolve versioning between Controller-Actions, everything else is handled by the regular ASP.NET Core routing, and behave as you're used to.

Configuration

In your Startup.cs you can configure what ways of API-versioning you want to support (all activated by default). You can also change the keys of the routing, query string, custom header and content type.

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureRouteVersions(options =>
    {        
        options.UseRoute = true;
        options.UseQuery = true;
        options.UseCustomHeader = true;
        options.UseAcceptHeader = true;

        // Set route-name in template used. For example: "api/v{version}/[controller]"
        // Default: "api-version"
        options.RouteKey = "version";

        // Set query string-key used. For example: "/api/customers?v=1"
        // Default: "api-version"
        options.QueryKey = "v"; // To use: '/api/customers?v=1'

        // Set custom version header used. For example: "my-app-api-version"
        // Default: "api-version"
        options.CustomHeaderKey = "my-app-api-version";

        // Set Accept-header vendor used. For example: "application/vnd.my-custom-api-header.v1+json"
        // Default: "application/vnd.api-version.v1+json"
        options.SetAcceptHeader("my-custom-api-header");

        // Set Accept-header regex-pattern. For example: "application/pre.my-custom-vendor-api.v1+json"
        options.AcceptRegexPattern = @"application\/pre\.my-custom-vendor-api\.v([\d]+)\+json";
    });

    services.AddMvc();
}

Default version

If you know that your new version of an API-endpoint is compatible with previous version, and if you want to support it, you can use the IsDefault-parameter with the [RouteVersion]-attribute. For example, if you've just added new fields to the next version and you find that is compatible enough to be the default version of the API-endpoint:

[Route("api/[controller]")]
[ApiController]
public class CustomersController : ControllerBase
{
    [HttpGet]
    [RouteVersion(1)]
    public ActionResult<string> GetV1()
    {
        return "Get Customers Version 1";
    }

    [HttpGet]
    [RouteVersion(2, IsDefault = true)]
    public ActionResult<string> GetV2()
    {
        return "Get Customers Version 2";
    }
}

Then you can make a call to the URL for the API-endpoint without specifying the version and get the default version, which in this example is v2:

HTTP GET:
https://my-web-app.com/api/customers/
> "Get Customers Version 2"

Contributing

You can find the source code on GitHub, the newest unstable build on MyGet and the latest version of the library on NuGet

ASP.NET Core MVC SEO-Framework

Following my last post on my ASP.NET MVC SEO-Framework I started looking at adding support also for ASP.NET Core MVC, with its superior Dependency Injection and Tag Helpers.

The previous post shows examples on how to use attributes to set SEO-specific values for Controller-Actions and in Views, which is also used in ASP.NET Core MVC. What is new to Core MVC is how you register the SEO-helper as a Service for Dependency Injection and use Tag Helpers instead of HTML Helpers.

To register the SEO-helper as a service for Dependency Injection you just need to use the framework's provided extension-method in the ConfigureServices-method inside Startup.cs:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        services.AddSeoHelper();
    }
}

These SEO-values can easily be accessed and rendered as HTML-tags inside Views through provided Tag Helpers:

<head>
    <seo-title />

    <seo-link-canonical />
    <seo-meta-description />
    <seo-meta-keywords />
    <seo-meta-robots-index />
</head>

To access these Tag Helpers you need to reference them in you _ViewImports.cshtml:

@addTagHelper *, AspNetSeo.CoreMvc
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

See the README-file on GitHub for the latest detailed information about this ASP.NET SEO-framework. Or try it out through Nuget by running Install-Package AspNetSeo.CoreMvc or Install-Package AspNetSeo.Mvc in your project. You can even follow the absolutely latest build on MyGet.

ASP.NET MVC SEO-Framework

For any serious web-application you should always implement a solid Search engine optimization-strategy, but there is no standardized way to handle this in ASP.NET MVC, out of the box.

You could easily use the ViewBag-object to send dynamic values from Controller-Actions into Views, like this, for example:

public ActionResult Index()
{
    this.ViewBag.PageTitle = "This is the page title";

    return this.View();
}

Then you'd have to make sure you correctly spell or copy-paste ViewBag.PageTitle correctly into your View:

<head>
    <title>@ViewBag.PageTitle</title>
    <!-- More head-values -->
</head>

One problem is that if you refactor the naming for ViewBag.PageTitle into, for example ViewBag.Title, this will break the functionality, potentially website-wide, because you won't get any tooling-help from Visual Studio for that rename.

This is why I created a framework for ASP.NET MVC SEO, to get structured and reusable functionality around the SEO-data for a web-application. The framework is available on Nuget, with the source-code on GitHub.

Setting SEO-values

You can set SEO-values using the properties on a SeoHelper-object in Controller-Actions and Views, or you can use ActionFilter-attributes in Controllers, to set SEO-related data like:

  • Meta-Description
  • Meta-Keywords
  • Title, split on page-title and base-title (website-title)
  • Canonical Link
  • Meta No-index for robots

This can be done inside Controllers and Controller-Actions:

[SeoBaseTitle("Website name")]
public class InfoController : SeoController
{
    [SeoTitle("Listing items")]
    [SeoMetaDescription("List of the company's product-items")]
    public ActionResult List()
    {
        var list = this.GetList();

        if (list.Any())
        {
            this.Seo.Title += $" (Total: {list.Count})";
            this.Seo.LinkCanonical = "~/pages/list.html";
        }
        else
        {
            this.Seo.MetaRobotsNoIndex = true;
        }

        return this.View(model);
    }
}

If you don't want to inherit from SeoController to get access to the this.Seo-property, you can use the extension-method GetSeoHelper:

public class InfoController : Controller
{
    public ActionResult List()
    {
        var seo = this.GetSeoHelper();

        seo.Title = "Page title";

        return this.View(model);
    }
}

You can even set SEO-values inside Views:

@{
    this.Layout = null;
    this.Seo.MetaRobotsNoIndex = true; // Always block Robots from indexing this View
}

Rendering SEO-values

These SEO-values can easily be accessed and rendered as HTML-tags inside Views through provided HtmlHelper-extensions:

<head>
    @Html.SeoTitle()

    @Html.SeoLinkCanonical()
    @Html.SeoMetaDescription()
    @Html.SeoMetaKeywords()
    @Html.SeoMetaRobotsIndex()
</head>

See the README-file on GitHub for the latest detailed information about this ASP.NET MVC SEO-framework. Or try it out through Nuget by running Install-Package AspNetSeo.Mvc in your project. You can even follow the absolutely latest build on MyGet.

NullableGuidConstraint for ASP.NET MVC & WebApi

Have you ever written a very usable Route-constraint in ASP.NET MVC or in WebAPI than you wanted to share between them both? For example a constraint that supports nullable Guids (Guid?) as route-parameter.

This can be done by implementing both System.Web.Routing.IRouteConstraint and System.Web.Http.Routing.IHttpRouteConstraint.

Hopefully this article will be obsolete with the release of ASP.NET 5, but until then, here's how you solve this problem:

public class NullableGuidConstraint : IRouteConstraint, IHttpRouteConstraint
{
    // ASP.NET MVC-signature
    public bool Match(
        HttpContextBase httpContext,
        Route route,
        string parameterName,
        RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        return MatchInternal(parameterName, values);
    }

    // WebAPI-signature
    public bool Match(
        HttpRequestMessage request,
        IHttpRoute route,
        string parameterName,
        IDictionary values,
        HttpRouteDirection routeDirection)
    {
        return MatchInternal(parameterName, values);
    }

    private static bool MatchInternal(string parameterName, IDictionary values)
    {
        object value;
        if (!values.TryGetValue(parameterName, out value))
        {
            return false;
        }

        if (value is Guid)
        {
            return true;
        }

        string stringValue = Convert.ToString(value, CultureInfo.InvariantCulture);

        Guid guid;
        bool isMatch = string.IsNullOrWhiteSpace(stringValue) || Guid.TryParse(stringValue, out guid);
        return isMatch;
    }
}

Then you register this constraint through, for example, DefaultInlineConstraintResolver in System.Web.Mvc.Routing for ASP.NET MVC and System.Web.Http.Routing for WebAPI.

var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("guid?", typeof(NullableGuidConstraint));

// ASP.NET MVC
routes.MapMvcAttributeRoutes(constraintResolver);

// WebAPI
routes.MapHttpAttributeRoutes(constraintResolver);

Now you can write attribute-routes like this:

[Route("{controller}/{action}/{id=guid?}")]

Render .ascx-Files in ASP.NET MVC Using Only RazorViewEngine

ASP.NET Band-Aid

If you're stuck in an environment where you're migrating from ASP.NET MVC to ASP.NET WebForms it's good to know that you can actually render your existing WebForms-Controls in you MVC-views. This might sound like a crazy thing to do (and it is in the long run!) but it might be useful if you're stuck between sprints and have perfectly working WebForms-Controls (.ascx-files) that you don't have time to migrate right now. All you have to do is use the HtmlHelper's helper-method .RenderPartial(string partialViewName) and pass it the path to the WebForms-Control.

// Write the content of a control inside a view:
Html.RenderPartial("~/Controls/ControlVirtualPath.ascx");

// Or to get the content of a control as a MvcHtmlString, for further manipulation:
Html.Partial("~/Controls/CustomControl.ascx")

It's important that your controls inherits from System.Web.Mvc.ViewUserControl and NOT the old System.Web.UI.UserControl.

One performance-tip that is often mentioned around ASP.NET MVC is to deactivate the WebForms-View Engine for MVC Razor-views (which actually turns out to maybe not make such a big difference after all). This will not prevent .aspx, .ascx and other WebForms-files from working.

But you still want your .ascx-files to work inline in your MVC Razor-views. This can be achieved by implementing your own class that inherits RazorViewEngine, which only uses the WebFormViewEngine when actually needed.

Global.asax (or other config-class)

// Remove WebFormViewEngine (and RazorViewEngine)
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomRazorViewEngine());

CustomRazorViewEngine : System.Web.Mvc.RazorViewEngine

private static readonly WebFormViewEngine WebFormsEngine = new WebFormViewEngine();

public override ViewEngineResult FindPartialView(
    ControllerContext context, string name, bool useCache)
{
    if (name.EndsWith(".ascx"))
    {
        return WebFormsEngine.FindPartialView(context, name, useCache);
    }

    return base.FindPartialView(context, name, useCache);
}

If you need the actual class of the control to do some further analysis/manipulation, you can do the following anywhere in your code:

var viewPage = new ViewPage();
var control = viewPage.LoadControl("~/Controls/ControlVirtualPath.ascx") as ControlType;