How does javascript version (asp-append-version) work in ASP.NET Core MVC?
Asked Answered
C

3

79

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.

Chartism answered 26/10, 2015 at 9:46 Comment(0)
E
117

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
Edwyna answered 26/10, 2015 at 17:30 Comment(3)
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?Egwin
@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.Edwyna
As I understand, asp-add-version is not supported in Blazor (see github.com/dotnet/aspnetcore/issues/23842). What should I use instead?Barocchio
W
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);
    }
}
Weltpolitik answered 13/7, 2017 at 18:51 Comment(2)
From 2.2 onwards, I had to replace this with ... var versionProvider = context.RequestServices.GetRequiredService<IFileVersionProvider>(); return versionProvider.AddFileVersionToPath(context.Request.Path, path);Rika
@Rika 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.Weltpolitik
B
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.

Bilge answered 1/3, 2017 at 9:6 Comment(1)
then what is solution for absolute path?Flirtation

© 2022 - 2024 — McMap. All rights reserved.