受欢迎的博客标签

custom Static Content middleware in ASP.NET Core

Published

ASP.NET Core Architecture
The architecture of ASP.NET Core allow us to be at any time, part of the pipeline of the content generation.

One of this piece of this architecture is a Middleware. What’s a middleware? It’s an element of a pipeline that is ran before and after the actual user code. Elements of a pipeline are executed in order and call the next one in the pipeline. This allow us to run pre/post within the same class.

The theory here is, if we are high enough in the pipeline, we can intercept calls after they reach ASP.NET MVC to generate our files but low enough so that Kestrel can still serve our static files.

Startup.cs
First, we need to ensure that we set our Startup.cs properly. The first middleware are going to check for the default files. If they are found, they will stop the pipeline and just serve the file. If they are not found, they will get our middleware and finally MVC.

app.UseDefaultFiles(); // <== this is not present by default. Add it.
app.UseStaticFiles();

// ********* This is where we want to be ********
app.UseMiddleware<StaticGeneratorMiddleware>(); // <=== we'll create this middleware in a minute

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

Here’s how it would look like visually.

As the request comes in, each middleware is executed in order and once the bottom of the pipeline is reached, each middleware gets to execute one last time on the way up.

Creating StaticGeneratorMiddleware

public class StaticGeneratorMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IHostingEnvironment _hostingEnvironment;

    public StaticGeneratorMiddleware(RequestDelegate next, IHostingEnvironment hostingEnvironment)
    {
        if (next == null) throw new ArgumentNullException(nameof(next));
        if (hostingEnvironment == null) throw new ArgumentNullException(nameof(hostingEnvironment));

        _next = next;
        _hostingEnvironment = hostingEnvironment;
    }

    public async Task Invoke(HttpContext context)
    {
        if (context == null) throw new ArgumentNullException(nameof(context));
        if (context.Request == null) throw new ArgumentNullException(nameof(context.Request));

        // we skip non-html content for now


        // we skip the first slash and we reverse the slashes
        var baseUrl = context.Request.Path.Value.Substring(1).Replace("/", "\\");
        // default files will look for "index.html"
        var destinationFile = Path.Combine(_hostingEnvironment.ContentRootPath, "staticgen", baseUrl, "index.html");


        // replace the output stream to collect the result
        var responseStream = context.Response.Body;
        var buffer = new MemoryStream();
        var reader = new StreamReader(buffer);
        context.Response.Body = buffer;
        try 
        {
            // execute the rest of the pipeline
            await _next(context);
        
            if (context.Response?.ContentType?.Contains("text/html") == false && context.Response.StatusCode != 200)
            {
                await _next(context);
                return;
            }
        
            EnsureDestinationFolderExist(destinationFile);
        
            // reset the buffer and retrieve the content
            buffer.Seek(0, SeekOrigin.Begin);
            var responseBody = await reader.ReadToEndAsync();
        
            // output the content to disk
            await WriteBodyToDisk(responseBody, destinationFile);
        
            // copy back our buffer to the response stream
            buffer.Seek(0, SeekOrigin.Begin);
            await buffer.CopyToAsync(responseStream);
        }
        finally
        {
            // Workaround for https://github.com/aspnet/KestrelHttpServer/issues/940
            context.Response.Body = responseStream;
        }
    }

    private void EnsureDestinationFolderExist(string destinationFile)
    {
        var directoryName = Path.GetDirectoryName(destinationFile);
        Directory.CreateDirectory(directoryName);
    }

    private async Task WriteBodyToDisk(string responseBody, string destinationFile)
    {
        using (FileStream fs = new FileStream(destinationFile, FileMode.Create))
        using (StreamWriter sw = new StreamWriter(fs))
        {
            await sw.WriteAsync(responseBody);
        }
    }
}

 

Another middleware that should be added above UseDefaultFiles and UseStaticFiles is a middleware that would delete files after certain conditions. Otherwise, we will never regerate those files.

Creating files in a separate folder
First, you’ll need to update your UseStaticFiles to look like this:

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new CompositeFileProvider(
        new PhysicalFileProvider(env.WebRootPath),
        new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "staticgen"))
    )
});

Then, you will need to adapt the middleware to generate it in the proper directory.

var destinationFile = Path.Combine(_hostingEnvironment.ContentRootPath, "staticgen", baseUrl, "index.html");