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" />