Supporting HTML PushState in ASP.NET VNext with custom Middleware

Many modern frameworks like for example Angular and Aurelia support HTML5 PushState. This allows you to remove the # from the uri.

There are some steps to enable this for IIS, like adding the following to the system.webServer section of the web.config located in the wwwroot directory.

<rewrite>  
    <rules>
        <rule name="Main Rule" stopProcessing="true">
            <match url=".*" />
            <conditions logicalGrouping="MatchAll">                       
                <!-- Static files and directories can be served so partials etc can be loaded -->
                <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
            </conditions>
            <action type="Rewrite" url="/" />
        </rule>
    </rules>
</rewrite>  

But they won't work in a Kestrel only situation / won't allow you to run dnx web from the command line, because Kestrel isn't IIS.

What we want (or what the URL rewrite in IIS does) is the following:

  • Check if a static file exists and can be served.
  • If it returns 404, serve static file index.html.

To get this working, you need to write some custom middleware, which re-uses the existing StaticFileMiddleware class.

public class PushStateStaticFileMiddleware  {

    private RequestDelegate next;
    private StaticFileMiddleware middleware;

    public PushStateStaticFileMiddleware(RequestDelegate next, 
    IHostingEnvironment hostingEnv, StaticFileOptions options, ILoggerFactory loggerFactory) 
    {
        this.next = next;
        this.middleware = new StaticFileMiddleware(next, hostingEnv, options, loggerFactory);

    }

    public Task Invoke(HttpContext context) {

        this.middleware.Invoke(context).ContinueWith(task => {
            if (context.Response.StatusCode == 404) {
                context.Response.StatusCode = StatusCodes.Status200OK;
                context.Request.Path = "/index.html";
                this.middleware.Invoke(context);
            }
        }).Wait();

        return this.next(context);
    }
}

Next we need to write some extentions to enable you to use the middleware in the following way:

app.UseStaticFilesPushState();  

It's basicly the same as the extentions for the StaticFileMiddleware, because we still redirect calls to this class.

public static class StaticFileExtensions  
{
    /// <summary>
    /// Enables static file serving for the current request path
    /// </summary>
    /// <param name="builder"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseStaticFilesPushState(this IApplicationBuilder builder)
    {
        return builder.UseStaticFilesPushState(new StaticFileOptions());
    }

    /// <summary>
    /// Enables static file serving for the given request path
    /// </summary>
    /// <param name="builder"></param>
    /// <param name="requestPath">The relative request path.</param>
    /// <returns></returns>
    public static IApplicationBuilder UseStaticFilesPushState(this IApplicationBuilder builder, string requestPath)
    {
        return builder.UseStaticFilesPushState(new StaticFileOptions() { RequestPath = new PathString(requestPath) });
    }

    /// <summary>
    /// Enables static file serving with the given options
    /// </summary>
    /// <param name="builder"></param>
    /// <param name="options"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseStaticFilesPushState(this IApplicationBuilder builder, StaticFileOptions options)
    {
        return builder.UseMiddleware<PushStateStaticFileMiddleware>(options);
    }
}

When this is all added, add the middleware to your application by putting it in your Startup.cs file.

public class Startup  
{
    public Startup(IHostingEnvironment env)
    {
        // Set up configuration sources.

        var builder = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

        builder.AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; set; }

    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseIISPlatformHandler(options => options.AuthenticationDescriptions.Clear());
        app.UseDefaultFiles();
        app.UseStaticFilesPushState();
    }

    // Entry point for the application.
    public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}