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

3

8

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?

Chemmy answered 13/3, 2017 at 23:21 Comment(1)
You can generate version directly using extension AddFileVersionToPath from How does javascript version (asp-append-version) work in ASP.NET Core MVCHumphreys
D
10

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" />
Disquietude answered 21/5, 2020 at 0:9 Comment(0)
L
2

Consider removing the /bower/ prefix when referring to jquery/dist/jquery.min.js as follows:

<script type="text/javascript" src="~/jquery/dist/jquery.min.js"></script>

Also, you should set the HostingEnvironment.WebRootFileProvider in the Startup.Configure method as follows:

var compositeProvider = new CompositeFileProvider(
    env.WebRootFileProvider,
    new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "bower_components")));
env.WebRootFileProvider = compositeProvider;
var options = new StaticFileOptions()
{
    FileProvider = compositeProvider,
    RequestPath = "/bower"
};
app.UseStaticFiles(options);

Hope this helps.

Lavella answered 9/3, 2018 at 23:13 Comment(2)
Artak, this solution makes it work, but it's not ideal. Combining different folders with wwwroot and pretend that all static files are coming from the root is conceptually incorrect. The correct way to do it for the majority of cases is to like IIS does it with virtual directories, it adds them to the root. So files coming from wwwroot will be served from / and files coming from other folders (bower_components in OP case) will be served from the assigned names under root (/bower in OP case).Disquietude
CompositeFileProvider doesn't allow that, so another provider should be added which accepts StaticFileOptions (or similar). I know that implementing such a provider is relatively easy, but I think it's a basic functionality that should be part of Core. People shouldn't have to remove the prefix (/bower in OP case) to make it work.Disquietude
C
0

What am I doing wrong?

Nothing. According to ASP.NET Core's source, they create a FileVersionProvider which starts from the WebRootPath or wwwroot for this task:

private void EnsureFileVersionProvider()
{
    if (_fileVersionProvider == null)
    {
        _fileVersionProvider = new FileVersionProvider(
            HostingEnvironment.WebRootFileProvider,
            Cache,
            ViewContext.HttpContext.Request.PathBase);
    }
}
Cicely answered 3/8, 2017 at 17:6 Comment(5)
Looking at code it seems everything related to that FileVersionProvider is private. I don't know a lot about tag helpers: is there any way to inherit and extend original tag helper? Or extend this particular behavior somehow? ThanksSixtynine
I think it's a very good behavior. because it forces you to bundle your static files and then apply a version to that single file. you should not have a direct reference to the bower or node_js folders. Use github.com/madskristensen/BundlerMinifier for this task.Cicely
I agree with examples you provided, but let me dissent in general. We have a separate angular-cli client project (same for any webpack-based one) which compiles and bundles its own sources into classic dist folder. There's no way we're going to use BundleMinifier, and I'm not sure we should save client build output into asp.net wwwroot folder.Sixtynine
why not? set the outDir in .angular-cli.json to wwwroot and then use this solution: michaco.net/blog/…Cicely
Yeah I am aware you can. I'm just not sure we should. I'll have a thought about it. Thanks for pointing to that post, though.Sixtynine

© 2022 - 2024 — McMap. All rights reserved.