CSS- und JavaScript-Dateien automatisch mit Versionsnummer einbinden

, , ,

In ASP.NET MVC gibt es Url.Content um (z.B.) in der Datei „_Layout.cshtml“ Ressourcen einzubinden.

Also CSS- und JavaScript-Dateien.

So ein Aufruf schaut z.B. so aus:

<link href="@Url.Content(@"~/Content/bootstrap.min.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content(@"~/Scripts/bootstrap.min.js")"></script>

Es werden dann die tatsächlichen URLs zu den Dateien in der fertigen HTML-Seite generiert.

Also z.B.:

<link href="/Content/bootstrap.min.css" rel="stylesheet" type="text/css" />
<script src="/Scripts/bootstrap.min.js"></script>

Wenn es nun zu inhaltlichen Änderungen an den Dateien kommt, kann es passieren, dass die Dateien durch Browser-Caching beim Endanwender nicht ankommen, er also tendenziell falsches CSS und falsches JavaScript sieht.

Ein gern gemachter Trick ist, an die URLs das Änderungsdatum der Dateien anzuhängen.

Ändern sich die Dateien, ändern sich auch die URLs und der Browser-Cache wird umgangen.

Nutzt Ihr die Bundle-Funktionalität von ASP.NET MVC, wird dies automatisch schon so gemacht.

Verwendet Ihr keine Bundles, gibt es keine eingebaute Lösung.

Aus diesem Grund habe ich basierend auf diesem Stack-Overflow-Posting eine kleine Extension geschrieben:

public static class TimestampedContentExtensions
{
    public static string VersionedContent(this UrlHelper helper, string contentPath)
    {
        var context = helper.RequestContext.HttpContext;

        if (context.Cache[contentPath] == null)
        {
            var physicalPath = context.Server.MapPath(contentPath);
            var version = @"v=" + new FileInfo(physicalPath).LastWriteTime.ToString(@"yyyyMMddHHmmss");

            var translatedContentPath = helper.Content(contentPath);

            var versionedContentPath =
                contentPath.Contains(@"?")
                    ? translatedContentPath + @"&" + version
                    : translatedContentPath + @"?" + version;

            context.Cache.Add(physicalPath, version, null, DateTime.Now.AddMinutes(1), TimeSpan.Zero,
                CacheItemPriority.Normal, null);

            context.Cache[contentPath] = versionedContentPath;
            return versionedContentPath;
        }
        else
        {
            return context.Cache[contentPath] as string;
        }
    }
}

Diese könnt Ihr als Drop-In-Replacement verwenden. Obiger Code wird dann so geändert:

<link href="@Url.VersionedContent(@"~/Content/bootstrap.min.css")" rel="stylesheet" type="text/css" />
<script src="@Url.VersionedContent(@"~/Scripts/bootstrap.min.js")"></script>

Und das generierte HTML sieht dann so aus:

<link href="/Content/bootstrap.min.css?v=20151104105858" rel="stylesheet" type="text/css" />
<script src="/Scripts/bootstrap.min.js?v=20151029213517"></script>

Das ist für mich eine sehr nützliche Funktion geworden.

Falls Ihr die Erweiterung nutzen wollt, könnt Ihr ggf. noch Fehler-Handling einbauen, für den Fall dass contentPath gar keine physikalische Datei ist und sowohl MapPath also auch der FileInfo-Konstruktor dann fehlschlagen würden.