Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core's asp-append-version attribute not working for static files outside of the wwwroot directory

I have an ASP.NET Core project with static files in both the wwwroot directory and bower_components directory.

I am able to server these files by adding this to my Startup.cs class:

StaticFileOptions rootFileOptions = new StaticFileOptions();
rootFileOptions.OnPrepareResponse = staticFilesResponseHandler;
StaticFileOptions bowerFileOptions = new StaticFileOptions();
bowerFileOptions.OnPrepareResponse = staticFilesResponseHandler;
string bowerDirectory = Path.Combine(Directory.GetCurrentDirectory(), "bower_components");
PhysicalFileProvider bowerPhysicalFileProvider = new PhysicalFileProvider(bowerDirectory);
bowerFileOptions.FileProvider = bowerPhysicalFileProvider;
bowerFileOptions.RequestPath = new PathString("/bower");
app.UseStaticFiles(rootFileOptions);
app.UseStaticFiles(bowerFileOptions);

And then reference them from my views as follows:

<script type="text/javascript" src="/bower/jquery/dist/jquery.min.js" asp-append-version="true"></script>
<script type="text/javascript" src="/Libs/jQuery-UI/jquery-ui.min.js" asp-append-version="true"></script>

Even though asp-append-version seems to work just fine for resources located under wwwroot, it seems to be completely ignored for resources outside of wwwroot. All resources are being properly served though; no 404s or anything. The resulting HTML for the code above is as follows:

<script type="text/javascript" src="/bower/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="/Libs/jQuery-UI/jquery-ui.min.js?v=YZKMNaPD9FY0wb12QiluqhIOWFhZXnjgiRJoxErwvwI"></script>

What am I doing wrong?

like image 897
AxiomaticNexus Avatar asked Mar 13 '17 23:03

AxiomaticNexus


1 Answers

This question is old, but the issue still exists and although the solution provided by Artak works, it is conceptually incorrect in most cases. First let's see the root of the problem:

asp-append-version looks for the files using IHostingEnvironment.WebRootFileProvider which by default is a PhysicalFileProvider pointing to the wwwroot folder.

The Core docs have an example on how to serve files outside of web root:

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles(); // For the wwwroot folder

    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(Directory.GetCurrentDirectory(), "MyStaticFiles")),
        RequestPath = "/StaticFiles"
    });
}

This allows you to server static files from both wwwroot and MyStaticFiles folders. If you have an image \MyStaticFiles\pic1.jpg, you can refer to it in two ways:

<img src="~/pic1.jpg" />
<img src="~/StaticFiles/pic1.jpg" />

Both will work equally. This is conceptually incorrect because you gave the path an alias of /StaticFiles, so its files shouldn't be combined with the root /. But at least it works and it gives you what you want.

Sadly, asp-append-version doesn't know about all of that. It should, but it doesn't. It should because it is meant to be used with static files (JavaScript, CSS, and images), so it makes sense that if we changed the configurations to serve static files from different folders, that asp-append-version gets a copy of those configurations. It doesn't, so we need to configure it separately by modify IHostingEnvironment.WebRootFileProvider.

Artak suggested to use CompositeFileProvider which allows us to assign more than one file provider to IHostingEnvironment.WebRootFileProvider. That does work, however it has a fundamental issue. CompositeFileProvider doesn't allow us to define the RequestPath like in StaticFileOptions. As a workaround, Artak suggested that we should not use the prefix, which makes use of the above mentioned incorrect behaviour that files can be referenced in both ways. To demonstrate the issue, let's say that the other folder has a structure like this:

|_ MyStaticFiles
       |_ HTML
       | |_ privacy.html
       | |_ faq.html
       |_ images
         |_ image1.jpg

Now, what happens to all files in the MyStaticFiles\images folder? Assuming that wwwroot also has images folder, will it work or give you an error for two identically named folders? Where will the file ~/images/image1.jpg be coming from?

Regardless of whether it works or not, there is often an important reason for why you have your static files in a folder other than wwwroot. It is often because those static files are for example content files that you don't want mixed with website design files.

We need a provider that allows us to specify the RequestPath for each folder. Since Core doesn't currently have such provider, we're only left with the option of writing our own. Although not difficult, it's not a task that many programmers like to tackle. Here is a quick implementation, it's not perfect, but it does the job. It is based on the example provided by Marius Zkochanowski with some inhancements:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Extensions.FileProviders {
  class CompositeFileWithOptionsProvider : IFileProvider {
    private readonly IFileProvider _webRootFileProvider;
    private readonly IEnumerable<StaticFileOptions> _staticFileOptions;

    public CompositeFileWithOptionsProvider(IFileProvider webRootFileProvider, params StaticFileOptions[] staticFileOptions)
  : this(webRootFileProvider, (IEnumerable<StaticFileOptions>)staticFileOptions) { }

    public CompositeFileWithOptionsProvider(IFileProvider webRootFileProvider, IEnumerable<StaticFileOptions> staticFileOptions) {
      _webRootFileProvider = webRootFileProvider ?? throw new ArgumentNullException(nameof(webRootFileProvider));
      _staticFileOptions = staticFileOptions;
    }

    public IDirectoryContents GetDirectoryContents(string subpath) {
      var provider = GetFileProvider(subpath, out string outpath);
      return provider.GetDirectoryContents(outpath);
    }

    public IFileInfo GetFileInfo(string subpath) {
      var provider = GetFileProvider(subpath, out string outpath);
      return provider.GetFileInfo(outpath);
    }

    public IChangeToken Watch(string filter) {
      var provider = GetFileProvider(filter, out string outpath);
      return provider.Watch(outpath);
    }

    private IFileProvider GetFileProvider(string path, out string outpath) {
      outpath = path;
      var fileProviders = _staticFileOptions;
      if (fileProviders != null) {
        foreach (var item in fileProviders) {
          if (path.StartsWith(item.RequestPath, StringComparison.Ordinal)) {
            outpath = path.Substring(item.RequestPath.Value.Length, path.Length - item.RequestPath.Value.Length);
            return item.FileProvider;
          }
        }
      }
      return _webRootFileProvider;
    }
  }
}

Now we can update Artak's example to use the new provider:

app.UseStaticFiles(); //For the wwwroot folder.
//This serves static files from the given folder similar to IIS virtual directory.
var options = new StaticFileOptions {
  FileProvider = new PhysicalFileProvider(Configuration.GetValue<string>("ContentPath")),
  RequestPath = "/Content"
};
//This is required for asp-append-version (it needs to know where to find the file to hash it).
env.WebRootFileProvider = new CompositeFileWithOptionsProvider(env.WebRootFileProvider, options);
app.UseStaticFiles(options); //For any folders other than wwwroot.

Here, I'm getting the path from the configurations file, because often it is even outside the app's folder altogether. Now you can reference your content files using /Content and not ~/. Example:

<img src="~/Content/images/pic1.jpg" asp-append-version="true" />
like image 130
Racil Hilan Avatar answered Sep 23 '22 16:09

Racil Hilan