71

It seems that there is no dynamic bundling supported in the new MVC (link), and it should be done using a gulp task. MVC supports some new attribute called asp-append-version, but I have not found any explanation on how it works. I suspect that it's calculating some hash of the file contents and even updates it after a file change. Is there any documentation on how it works?

I am also wondering how it detects the file changes or whether it just recalculates the hash each time the MVC parses razor markup.

Himanshu
  • 31,810
  • 31
  • 111
  • 133
Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207

3 Answers3

111

You can check the LinkTagHelper source code, where you will see it is basically adding a version query string to the href value via a FileVersionProvider:

if (AppendVersion == true)
{
    EnsureFileVersionProvider();

    if (Href != null)
    {
        output.Attributes[HrefAttributeName].Value = _fileVersionProvider.AddFileVersionToPath(Href);
    }
}

private void EnsureFileVersionProvider()
{
    if (_fileVersionProvider == null)
    {
        _fileVersionProvider = new FileVersionProvider(
                HostingEnvironment.WebRootFileProvider,
                Cache,
                ViewContext.HttpContext.Request.PathBase);
    }
}

The FileVersionProvider will calculate the hash of the file contents using the SHA256 algorithm. It will then url encode it and add it to the query string as in:

path/to/file?v=B95ZXzHiOuQJzhBoHlSlNyN1_cOjJnz2DFsr-3ZyyJs

The hash will be recalculated only when the file changes, as it is added to the cache but with an expiration trigger based on a file watcher:

if (!_cache.TryGetValue(path, out value))
{
    value = QueryHelpers.AddQueryString(path, VersionKey, GetHashForFile(fileInfo));
    var cacheEntryOptions = new MemoryCacheEntryOptions().AddExpirationToken(_fileProvider.Watch(resolvedPath));
    _cache.Set(path, value, cacheEntryOptions);
}

This watcher is provided by HostingEnvironment.WebRootFileProvider, which implements IFileProvider:

//
// Summary:
//     Creates a change trigger with the specified filter.
//
// Parameters:
//   filter:
//     Filter string used to determine what files or folders to monitor. Example: **/*.cs,
//     *.*, subFolder/**/*.cshtml.
//
// Returns:
//     An Microsoft.Framework.Caching.IExpirationTrigger that is triggered when a file
//     matching filter is added, modified or deleted.
IExpirationTrigger Watch(string filter);

Note: You can see the cached values yourself by inspecting the values in the IMemoryCache:

//give the link:
<link rel="stylesheet" asp-append-version="true" href="~/css/site.css" />

//You can check the cached version
this.Context.RequestServices.GetRequiredService<IMemoryCache>().Get("/css/site.css")

//Which will show a value like:
/css/site.css?v=B95ZXzHiOuQJzhBoHlSlNyN1_cOjJnz2DFsr-3ZyyJs
Prakash G. R.
  • 4,746
  • 1
  • 24
  • 35
Daniel J.G.
  • 34,266
  • 9
  • 112
  • 112
  • 5
    Looking at the source code, it appears that `asp-append-version` only works with `WebRootFileProvider`. Is there a way to make it work with files outside of the `wwwroot` directory served by a `PhysicalFileProvider`? – AxiomaticNexus Mar 13 '17 at 23:31
  • 3
    @AxiomaticNexus There doesn't seem to be any hook for you to use a different `FileVersionProvider` which you would create using the `IHostingEnvironment.ContentRootFileProvider` (even though `FileVersionProvider` depends on `IFileProvider` so in theory it would work fine with the `ContentRootProvider`). I guess the tag helper was designed with files from wwwroot in mind, maybe its worth reaching MS on github. – Daniel J.G. Mar 14 '17 at 08:11
  • As I understand, `asp-add-version` is not supported in Blazor (see https://github.com/dotnet/aspnetcore/issues/23842). What should I use instead? – Mike Feb 08 '21 at 09:09
22

ASP.NET Core 2.2 and later

In Razor

@{var fileVersion = Context.AddFileVersionToPath("./path/to/resource");}
<script src="@fileVersion"></script>;

Extension method

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;

public static class HttpContextExtensions
{
    public static string AddFileVersionToPath(this HttpContext context, string path)
    {
        return context
            .RequestServices
            .GetRequiredService<IFileVersionProvider>()
            .AddFileVersionToPath(context.Request.PathBase, path);
    }
}

ASP.NET Core 2.1 and earlier

In Razor

var fileversion = '@this.AddFileVersionToPath("/js/components/forms.js")';

Extension method

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.TagHelpers.Internal;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;

public static class IRazorPageExtensions
{
    public static string AddFileVersionToPath(this IRazorPage page, string path)
    {
        var context = page.ViewContext.HttpContext;
        IMemoryCache cache = context.RequestServices.GetRequiredService<IMemoryCache>();
        var hostingEnvironment = context.RequestServices.GetRequiredService<IHostingEnvironment>();
        var versionProvider = new FileVersionProvider(hostingEnvironment.WebRootFileProvider, cache, context.Request.Path);
        return versionProvider.AddFileVersionToPath(path);
    }
}
JJS
  • 6,431
  • 1
  • 54
  • 70
  • 1
    From 2.2 onwards, I had to replace this with ... `var versionProvider = context.RequestServices.GetRequiredService();` `return versionProvider.AddFileVersionToPath(context.Request.Path, path);` – FlukeFan Dec 17 '18 at 10:54
  • @FlukeFan I'd be happy to edit the answer to include a version of the code with the current version. Please provide the full code snippet or edit the answer so everyone can benefit. – JJS Dec 17 '18 at 17:01
16

According to the current implementation of FileVersionProvider, hash is added only to the relative file path, e.g. <script src="~/js/jquery.min.js" asp-append-version="true"></script> In case absolute path is used, e.g. https://code.jquery.com/jquery-3.1.1.js, the hash would not be added.