Search code examples
c#asp.net-coreasp.net-core-mvcasp-append-version

How does JavaScript version (asp-append-version) work in ASP.NET Core MVC?


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.


Solution

  • 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