This post describes how you can minify and cache static files in ASP.NET Core. You may want to do this in order to make your website faster and to improve in search rankings (SEO).
One important thing to think about before you cache static files is that you need a method to invalidate the cache when your static files changes. Every static file that you save can have a unique name like a GUID or you can append a parameter to the filename that is unique for every new version of the file. You can use a combination of these methods, images can have GUID names and you can use a version parameter for other files.
Bundler and minifier
I use a NuGet package called BuildBundlerMinifier that is created by Mads Kristensen to minify static files in my projects. You also need a Bundler & Minifier extension in Visual Studio to get a task that minifies files (Task Runner Explorer). When you have installed this package, a json file called bundleconfig.json is added to your project in Visual Studio. You can edit this file directly or minfy static files by right-clicking on them in Visual Studio. The contents of the bundleconfig.json file looks like this.
[
{
"outputFileName": "wwwroot/css/admin_default.min.css",
"inputFiles": [ "wwwroot/css/admin_default.css" ]
},
{
"outputFileName": "wwwroot/css/admin_layout.min.css",
"inputFiles": [ "wwwroot/css/admin_layout.css" ]
}
]
Invalidate cache
We need a method to invalidate cache for static files. We have a common class in which we have injected IMemoryCache and IWebHostEnvironment, we create a reference to a IFileProvider file_provider from the environment variable.
public CommonServices(IMemoryCache cache, IWebHostEnvironment environment)
{
this.cache = cache;
this.environment = environment;
this.file_provider = environment.WebRootFileProvider;
} // End of the constructor
Our method to invalidate cache will store the relative path to the static file and an MD5 hash of the file in memory cache. The file provider will watch for changes in the file.
/// <summary>
/// Get a file path with a version hash
/// </summary>
public string GetFilePath(string relative_path)
{
// Create a variable for a hash
string hash = "";
// Get the hash
if(this.cache.TryGetValue(relative_path, out hash) == false)
{
// Create an absolute path
string absolute_path = this.environment.WebRootPath + relative_path;
// Make sure that the file exists
if(File.Exists(absolute_path) == false)
{
return relative_path;
}
// Create cache options
MemoryCacheEntryOptions cache_entry_options = new MemoryCacheEntryOptions();
// Add an expiration token that watches for changes in a file
cache_entry_options.AddExpirationToken(this.file_provider.Watch(relative_path));
// Create a hash of the file
using (MD5 md5 = MD5.Create())
{
using (Stream stream = File.OpenRead(absolute_path))
{
hash = Convert.ToBase64String(md5.ComputeHash(stream));
}
}
// Insert the hash to cache
this.cache.Set(relative_path, hash, cache_entry_options);
}
// Return the url
return $"{relative_path}?v={hash}";
} // End of the GetFilePath method
Add references to static files
We will use our GetFilePath method when we add references to static files in our layout view, for images it might be better to use a timestamp as version parameter if the names not is unique. Images can be large and many, save the timestamp in your database and append it to the version parameter.
<environment names="Development">
<link href="@this.tools.GetFilePath(imageUrls.Get("small_icon"))" rel="icon" type="image/x-icon" />
<link href="@this.tools.GetFilePath("/css/standard_layout.css")" media="screen and (min-width:1344px)" rel="stylesheet" />
<link href="@this.tools.GetFilePath("/css/medium_layout.css")" media="only screen and (min-width:1024px) and (max-width:1343px)" rel="stylesheet" />
</environment>
<environment names="Staging,Production">
<link href="@this.tools.GetFilePath(imageUrls.Get("small_icon"))" rel="icon" type="image/x-icon" />
<link href="@this.tools.GetFilePath("/css/standard_layout.min.css")" media="screen and (min-width:1344px)" rel="stylesheet" />
<link href="@this.tools.GetFilePath("/css/medium_layout.min.css")" media="only screen and (min-width:1024px) and (max-width:1343px)" rel="stylesheet" />
</environment>
Services
We need to add a service for memory cache in the ConfigureServices method in the StartUp class to be able to cache relative paths and a hash.
// Add memory cache
services.AddDistributedMemoryCache();
We also need to add headers to static files that indicates that they can be cached by a browser and for how long they can be cached. We add this in the Configure method in the StartUp class.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Use static files
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// Cache static files for 30 days
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000");
ctx.Context.Response.Headers.Append("Expires", DateTime.UtcNow.AddDays(30).ToString("R", CultureInfo.InvariantCulture));
}
});
// More code ...
} // End of the Configure method
You can inspect headers for static files in Chrome under Network in Developer tools to make sure that they have correct values.
Startup.cs
using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Annytab.Middleware;
using Annytab.Repositories;
using Annytab.Options;
using Fotbollstabeller.Middleware;
using Microsoft.AspNetCore.Mvc;
namespace Fotbollstabeller
{
/// <summary>
/// This class handles application startup
/// </summary>
public class Startup
{
/// <summary>
/// Variables
/// </summary>
public IConfiguration configuration { get; set; }
/// <summary>
/// Create a new startup object
/// </summary>
/// <param name="configuration">A reference to configuration</param>
public Startup(IConfiguration configuration)
{
this.configuration = configuration;
} // End of the constructor method
/// <summary>
/// This method is used to add services to the container.
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
// Add the mvc framework
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
//services.AddRazorPages();
// Add memory cache
services.AddDistributedMemoryCache();
// Add redis distributed cache
if (configuration.GetSection("AppSettings")["RedisConnectionString"] != "")
{
services.AddDistributedRedisCache(options =>
{
options.Configuration = configuration.GetSection("AppSettings")["RedisConnectionString"];
options.InstanceName = "Fotbollstabeller:";
});
}
// Add the session service
services.AddSession(options =>
{
// Set session options
options.IdleTimeout = TimeSpan.FromMinutes(20d);
options.Cookie.Name = ".Fotbollstabeller";
options.Cookie.Path = "/";
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
// Create database options
services.Configure<DatabaseOptions>(options =>
{
options.connection_string = configuration.GetSection("AppSettings")["ConnectionString"];
options.sql_retry_count = 1;
});
// Create cache options
services.Configure<CacheOptions>(options =>
{
options.expiration_in_minutes = 240d;
});
// Add Authentication
services.AddAuthentication()
.AddCookie("Administrator", options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.Cookie.MaxAge = TimeSpan.FromDays(1);
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Events.OnRedirectToLogin = (context) =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.Redirect("/admin_login");
return Task.CompletedTask;
};
})
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("ApiAuthentication", null);
// Add clients
services.AddHttpClient();
// Add repositories
services.AddSingleton<IDatabaseRepository, MsSqlRepository>();
services.AddSingleton<IWebsiteSettingRepository, WebsiteSettingRepository>();
services.AddSingleton<IAdministratorRepository, AdministratorRepository>();
services.AddSingleton<IFinalRepository, FinalRepository>();
services.AddSingleton<IGroupRepository, GroupRepository>();
services.AddSingleton<IStaticPageRepository, StaticPageRepository>();
services.AddSingleton<IXslTemplateRepository, XslTemplateRepository>();
services.AddSingleton<ISitemapRepository, SitemapRepository>();
services.AddSingleton<IXslProcessorRepository, XslProcessorRepository>();
services.AddSingleton<ICommonServices, CommonServices>();
} // End of the ConfigureServices method
/// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Use redirection
app.UseMiddleware<RedirectMiddleware>();
// Use error handling
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseStatusCodePagesWithReExecute("/home/error/{0}");
}
// Use static files
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// Cache static files for 30 days
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=25920000");
ctx.Context.Response.Headers.Append("Expires", DateTime.UtcNow.AddDays(300).ToString("R", CultureInfo.InvariantCulture));
}
});
// Use sessions
app.UseSession();
//// For most apps, calls to UseAuthentication, UseAuthorization, and UseCors must
//// appear between the calls to UseRouting and UseEndpoints to be effective.
//app.UseRouting();
//// Use authentication and authorization middlewares
app.UseAuthentication();
//app.UseAuthorization();
//// Routing endpoints
//app.UseEndpoints(endpoints =>
//{
// endpoints.MapControllerRoute(
// "default",
// "{controller=home}/{action=index}/{id?}");
//});
// Routing
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=home}/{action=index}/{id?}");
});
} // End of the Configure method
} // End of the class
} // End of the namespace
CommonServices.cs
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Collections.Generic;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
namespace Annytab.Repositories
{
/// <summary>
/// This class is a container for common services
/// </summary>
public class CommonServices : ICommonServices
{
#region Variables
private readonly IMemoryCache cache;
private readonly IHostingEnvironment environment;
private readonly IFileProvider file_provider;
#endregion
#region Constructors
/// <summary>
/// Create a new repository
/// </summary>
public CommonServices(IMemoryCache cache, IHostingEnvironment environment)
{
// Set values for instance variables
this.cache = cache;
this.environment = environment;
this.file_provider = environment.WebRootFileProvider;
} // End of the constructor
#endregion
#region Get methods
/// <summary>
/// Get image urls for the domain
/// </summary>
/// <returns>A key string list with urls</returns>
public KeyStringList GetDomainImageUrls(bool showNoImageIcon)
{
// Create the list to return
KeyStringList imageUrls = new KeyStringList(5);
// Add images to the key string list
imageUrls.Add("background_image", "/images/background_image.jpg");
imageUrls.Add("default_logotype", "/images/default_logotype.jpg");
imageUrls.Add("mobile_logotype", "/images/mobile_logotype.jpg");
imageUrls.Add("big_icon", "/images/big_icon.jpg");
imageUrls.Add("small_icon", "/images/small_icon.jpg");
if (showNoImageIcon == true)
{
// Create the no image path
string noImagePath = "/images/no_image_wide.jpg";
// Get all the keys in the dictionary
List<string> keys = imageUrls.dictionary.Keys.ToList<string>();
// Loop all the keys
for (int i = 0; i < keys.Count; i++)
{
// Get the url
string url = this.environment.WebRootPath + imageUrls.Get(keys[i]);
// Check if the file exists
if (System.IO.File.Exists(url) == false)
{
imageUrls.Update(keys[i], noImagePath);
}
}
}
// Return the list
return imageUrls;
} // End of the GetDomainImageUrls method
/// <summary>
/// Get a file path with a version hash
/// </summary>
public string GetFilePath(string relative_path)
{
// Create a variable for a hash
string hash = "";
// Get the hash
if(this.cache.TryGetValue(relative_path, out hash) == false)
{
// Create an absolute path
string absolute_path = this.environment.WebRootPath + relative_path;
// Make sure that the file exists
if(File.Exists(absolute_path) == false)
{
return relative_path;
}
// Create cache options
MemoryCacheEntryOptions cache_entry_options = new MemoryCacheEntryOptions();
// Add an expiration token that watches for changes in a file
cache_entry_options.AddExpirationToken(this.file_provider.Watch(relative_path));
// Create a hash of the file
using (MD5 md5 = MD5.Create())
{
using (Stream stream = File.OpenRead(absolute_path))
{
hash = Convert.ToBase64String(md5.ComputeHash(stream));
}
}
// Insert the hash to cache
this.cache.Set(relative_path, hash, cache_entry_options);
}
// Return the url
return $"{relative_path}?v={hash}";
} // End of the GetFilePath method
#endregion
} // End of the class
} // End of the namespace
_standard_layout.cshtml
@using Microsoft.AspNetCore.Hosting
@using Annytab.Repositories
@using Annytab.Models
@inject IHostingEnvironment environment
@inject IStaticPageRepository static_page_repository
@inject ICommonServices tools
@{
// Get form values
List<BreadCrumb> breadCrumbs = ViewBag.BreadCrumbs;
KeyStringList imageUrls = this.tools.GetDomainImageUrls(false);
IList<StaticPage> staticPages = this.static_page_repository.GetAllActiveLinks("sort_value", "ASC");
}
<!DOCTYPE html>
<html lang="sv" prefix="og: https://ogp.me/ns#">
<head>
@*Title and meta tags*@
<title>@(ViewBag.Title + " - Fotbollstabeller.nu")</title>
<meta name="description" content="@ViewBag.MetaDescription" />
<meta name="keywords" content="@ViewBag.MetaKeywords" />
<link rel="canonical" href="@ViewBag.MetaCanonical" />
<meta name="robots" content="@(Context.Request.Host.Value != "fotbollstabeller.azurewebsites.net" ? ViewBag.MetaRobots : "noindex, nofollow")" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0,user-scalable=yes" />
<meta name="google" content="notranslate">
@*Facebook meta tags*@
<meta property="og:title" content="@ViewBag.Title" />
<meta property="og:description" content="@ViewBag.MetaDescription" />
<meta property="og:type" content="website">
<meta property="og:url" content="@ViewBag.MetaCanonical" />
<meta property="og:image" content="@($"http://www.fotbollstabeller.nu{imageUrls.Get("big_icon")}")" />
<meta property="og:site_name" content="Fotbollstabeller.nu" />
@*Resources*@
<environment names="Development">
<link href="@this.tools.GetFilePath(imageUrls.Get("small_icon"))" rel="icon" type="image/x-icon" />
<link href="@this.tools.GetFilePath("/css/standard_layout.css")" media="screen and (min-width:1344px)" rel="stylesheet" />
<link href="@this.tools.GetFilePath("/css/medium_layout.css")" media="only screen and (min-width:1024px) and (max-width:1343px)" rel="stylesheet" />
<link href="@this.tools.GetFilePath("/css/mobile_layout.css")" media="only screen and (max-width:1023px)" rel="stylesheet" />
<link href="@this.tools.GetFilePath("/css/default_style.css")" rel="stylesheet" />
</environment>
<environment names="Staging,Production">
<link href="@this.tools.GetFilePath(imageUrls.Get("small_icon"))" rel="icon" type="image/x-icon" />
<link href="@this.tools.GetFilePath("/css/standard_layout.min.css")" media="screen and (min-width:1344px)" rel="stylesheet" />
<link href="@this.tools.GetFilePath("/css/medium_layout.min.css")" media="only screen and (min-width:1024px) and (max-width:1343px)" rel="stylesheet" />
<link href="@this.tools.GetFilePath("/css/mobile_layout.min.css")" media="only screen and (max-width:1023px)" rel="stylesheet" />
<link href="@this.tools.GetFilePath("/css/default_style.min.css")" rel="stylesheet" />
</environment>
@* Google analytics *@
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-4388854-23"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'UA-4388854-23');
</script>
@* Google ads *@
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
@*Google mobile ads*@
<script>
(adsbygoogle = window.adsbygoogle || []).push({
google_ad_client: "ca-pub-3070633924070834",
enable_page_level_ads: true
});
</script>
</head>
<body>
@*Cookie consent*@
<div class="annytab-cookie-consent">
<div class="annytab-basic-padding">
<span class="annytab-cookie-consent-text">Cookies hjälper oss att tillhandahålla våra tjänster. Genom att använda våra tjänster samtycker du till att vi använder cookies.</span>
<button id="btnCookieConsent" type="button" class="annytab-basic-button">Jag förstår</button>
<a href="/home/page/sekretesspolicy" class="cookie-consent-text">Läs mer</a>
</div>
</div>
@*The background image*@
<img id="backgroundImage" alt="Fotbollstabeller.nu" src="@this.tools.GetFilePath(imageUrls.Get("background_image"))" class="annytab-background-image" />
@*Master outer container*@
<div class="annytab-layout-outer-container">
@*Master inner container*@
<div class="annytab-layout-inner-container">
@*Standard header*@
<div class="annytab-layout-standard-header">
<a href="/"><img alt="Fotbollstabeller.nu, logotype" class="annytab-logotype" src="@this.tools.GetFilePath(imageUrls.Get("default_logotype"))" /></a>
</div>
@*Mobile header*@
<div class="annytab-layout-mobile-header">
<div id="toggleMobileMenu" class="annytab-layout-header-left"><i class="fas fa-bars fa-2x" aria-hidden="true"></i></div>
<div class="annytab-basic-divider annytab-layout-header-right"></div>
<a href="/"><img alt="Fotbollstabeller.nu, logotype" class="annytab-logotype" src="@this.tools.GetFilePath(imageUrls.Get("mobile_logotype"))" /></a>
</div>
@* Breadcrumbs *@
@if (breadCrumbs.Count > 0)
{
<div class="annytab-breadcrumb-container">
@for (int i = 0; i < breadCrumbs.Count; i++)
{
if (i > 0)
{
<span class="annytab-basic-text-normal"> // </span>
}
<a href="@breadCrumbs[i].link" rel="nofollow" class="annytab-basic-text-normal">@breadCrumbs[i].name</a>
}
</div>
}
@*Menu*@
<div id="menu" class="annytab-layout-menu">
<div class="annytab-layout-menu-padding">
@*Links*@
<a href="@("/")" class="annytab-menu-link not-hover">Startsidan</a>
@foreach (StaticPage post in staticPages)
{
<a href="@("/home/page/" + post.page_name)" class="annytab-menu-link not-hover">@post.link_name</a>
}
</div>
</div>
@*Middle container*@
<div class="annytab-layout-main-content">
<div class="annytab-basic-padding">
@RenderBody()
</div>
</div>
@*Footer*@
<div class="annytab-layout-footer">
<div class="annytab-basic-padding">
<span>@Html.Raw("© 2015 | Fotbollstabeller.nu")</span>
</div>
</div>
</div>
@*Right container*@
<div class="annytab-layout-right-content">
<div class="annytab-layout-right-ad">
<!-- fotbollstabeller.nu - 300x600 -->
<ins class="adsbygoogle"
style="display:block;width:300px;height:600px"
data-ad-client="ca-pub-3070633924070834"
data-ad-slot="3508160602"></ins>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>
</div>
</div>
@* Bottom spacing *@
<div class="annytab-layout-bottom-spacing"></div>
</div>
<div id="fb-root"></div>
@*Scripts*@
<environment names="Development">
<script src="/js/jquery/v3.3.1/jquery.js"></script>
<script src="/js/font-awesome/v5.0.8/fontawesome-all.js"></script>
<script src="@this.tools.GetFilePath("/js/annytab-front/annytab.default-functions.js")"></script>
</environment>
<environment names="Staging,Production">
<script src="/js/jquery/v3.3.1/jquery.min.js"></script>
<script src="/js/font-awesome/v5.0.8/fontawesome-all.min.js"></script>
<script src="@this.tools.GetFilePath("/js/annytab-front/annytab.default-functions.min.js")"></script>
</environment>
@*Facebook javascript*@
<script async defer crossorigin="anonymous" src="https://connect.facebook.net/sv_SE/sdk.js#xfbml=1&version=v4.0&autoLogAppEvents=1"></script>
@*Twitter javascript*@
<script>
window.twttr = (function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0],
t = window.twttr || {};
if (d.getElementById(id)) return t;
js = d.createElement(s);
js.id = id;
js.src = "https://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
t._e = [];
t.ready = function (f) {
t._e.push(f);
};
return t;
}(document, "script", "twitter-wjs"));
</script>
@RenderSection("scripts", required: false)
</body>
</html>
your example not working
please include with cs file
Hi, I have included cs-files at the end of the post.
You should be add highlight for important lines code.