How to serve Static Files with ASP.NET Core

In this article, let's look at how we can have these static files properly maintained and efficiently served for various purposes.

Introduction

Static files are asset files such as css, javascript and plain HTML files in addition to the document files such as word or pdf documents and images which have their states unchanged over the time.

These files are generally preserved for various scenarios and need to be served over the Internet; either as a part of rendering in the dynamic views or for having them available for download for the end users.

In this article, let’s look at how we can have these static files properly maintained and efficiently served for various purposes.

How to serve Static Files with ASP.NET Core

Imagine we have an application which has a form to upload documents for various usecases and later have them served for requesting end users. And in such scenarios we would need to have a mechanism to efficiently store the files received over the Internet and then serve them back for GET requests.

In general scenarios, the assets on the web server are not by default access to read as individual files; since the webserver treats all of the files as executables or endpoints. The individual existence of files along with their paths are to be explicitly dictated to the webservers. Sometimes we do so by means of MIME types; such as text/json, text/javascript and so on.

This was a practice from the web.config files used in the legacy ASP.NET applications. However, with the introduction of dotnetcore platform which has an extensive use of middlewares to do various jobs at ease, the handling of static files from the web server is taken off by a specific middleware which is added to the request pipeline via the Configure() method.

Serving Static Files with an Example

In a typical aspnetcore MVC application, we find the wwwroot folder which contains sub directories for css, js and other “assets” which are directly used in the view components. If we observe a default header and footer css files generated by the default template of the mvc, created by using the below command in dotnetcore CLI

> dotnet new mvc --name ReadersMvcApp

The files can contain the below code snippets;

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - ReadersMvcApp</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
	<!-- some default code -->
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>
    <footer class="border-top footer text-muted">
	<!-- some default code -->
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>

There are several css and javascript “assets” which are referenced similar to below

<script src="~/lib/jquery/dist/jquery.min.js"></script>

And these files are served from the wwwroot folder which contains subfolders to hold various css and js libraries. And how does the webserver knows that these are individual files and NOT endpoints? These are treated as individual filepaths and not endpoints because we add the below middleware to the Configure() method.

namespace ReadersMvcApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }


        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
		---- default code ----	

            app.UseStaticFiles();

		---- default code ----
        }
    }
}

This middleware internally configures the @wwwroot folder as an asset source, and directs the webserver to look for any individual asset file we indicate by using the “~/..” path to translate into “/wwwroot/..” path and serve the specified file as it is. This is what we call as “static file serving”.

How to serve Static Files from a Custom Path

Let’s assume that in my application of document upload tool, I need to upload the files to a custom folder in the root directory called “/documents” and not into the “@wwwroot” folder which is the default look-into folder for the webserver when we apply the UseStaticFiles() middleware.

In this case, we make use of a parameterized overload of the UseStaticFiles() method which accepts an argument of type StaticFileOptions. In the constructor of the StaticFileOptions we define the look-into path and the necessary properties or filters.

namespace ReadersMvcApp
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {            
            // default @wwwroot
            app.UseStaticFiles();

            // explicitly specifying to consider the wwwroot folder 
            // as a static folder
            app.UseStaticFiles(new StaticFileOptions()
            {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot"))
            });

            // specifying the folder "Files" as a static folder 
            // for any request path containing "/files"
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(Directory.GetCurrentDirectory(), "Files")),
                RequestPath = "/Files"
            });
		
		----- default code ----
        }
    }
}

How to cache Static Files

Now that we have looked into how we can specify the locations for the static folders, its equally important to specify for how much duration we need to let the web browser preserve these files for a request. Let’s assume that we have a static asset file configuration.json we serve to requesting users under the /assets/ request as below:

.../assets/config/configuration.json

returns a file response of MIME type JSON.

Now the nature of the file is such that for any new configuration edits, the file tends to be overwritten with newer versions. And the nature of the browsers shall be such that they tend to request all the assets again and again if they don’t tend to change over the time.

For such cases, we ask the browsers to cache the assets served from the web server for a period of time, assuring that they won’t change till that time. This we call as the “Cache-Control” which is maintained by sending a Response header along with the file response to the client browser which contains the below line:

Cache-Control: public,max-age=3600

Which means that the served asset response is not gonna change for the next 3600 minutes and is okay to be cached.

Whenever the client browser looks at this response header, it preserves the response asset for future requests and serves the same from local copy instead of making fresh HTTP calls. This results in faster page load times and effective data savings.

We implement the same in a StaticFiles middleware by adding additional response headers before serving the file back to the client request.

namespace ReadersMvcApp
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // default @wwwroot
            app.UseStaticFiles();

            // explicitly specifying to consider the wwwroot folder 
            // as a static folder
            app.UseStaticFiles(new StaticFileOptions()
            {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot"))
            });

            // specifying the folder "Files" as a static folder 
            // for any request path containing "/files"
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(Directory.GetCurrentDirectory(), "Files")),
                RequestPath = "/Files"
            });

            // specifying the Response Headers for 
            // all assets served via StaticFiles.
	    app.UseStaticFiles(new StaticFileOptions
            {
                OnPrepareResponse = ctx =>
                {
                    const int durationInSeconds = 60 * 60;
                    ctx.Context.Response.Headers[HeaderNames.CacheControl] 
                                  = "public,max-age=" + durationInSeconds;
                }
            });

		----- default code ----            
        }
    }
}

You may ask why we are adding multiple StaticFiles() method calls to the pipeline.

Conclusion

For every time a UseStaticFiles() middleware is called one specific addition is being made and since all these middlewares are chained to the application request pipeline, all these settings shall be applied to any incoming request for an asset which satisfies any of the request paths specified within these middlewares.

In this way, we can get started on serving assets from a dotnetcore web application from any custom folder within the directory by means of the UseStaticFiles() middleware which provides a subtle approach without having to mess up with the webserver configuration files.

We can further enhance this by means of Authorization or having directory browsing abilities, but that’s a story for another day.


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *