How to cache static files in ASP.NET Core? - vue.js

I can't seem to enable caching of static files in ASP.NET Core 2.2. I have the following in my Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseCors(...);
}
else {
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseSignalR(routes => { routes.MapHub<NotifyHub>("/..."); });
app.UseResponseCompression();
app.UseStaticFiles();
app.UseSpaStaticFiles(new StaticFileOptions() {
OnPrepareResponse = (ctx) => {
ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public, max-age=31557600"; // cache for 1 year
}
});
app.UseMvc();
app.UseSpa(spa => {
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment()) {
spa.UseVueCli(npmScript: "serve", port: 8080);
}
});
}
When I try and Audit the production site on HTTPS using chrome I keep getting "Serve static assets with an efficient cache policy":
In the network tab there is no mention of caching in the headers, when I press F5 it seems everything is served from disk cache. But, how can I be sure my caching setting is working if the audit is showing its not?

This is working in ASP.NET Core 2.2 to 3.1:
I know this is a bit similar to Fredrik's answer but you don't have to type literal strings in order to get the cache control header
app.UseStaticFiles(new StaticFileOptions()
{
HttpsCompression = Microsoft.AspNetCore.Http.Features.HttpsCompressionMode.Compress,
OnPrepareResponse = (context) =>
{
var headers = context.Context.Response.GetTypedHeaders();
headers.CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(30)
};
}
});

I do not know what UseSpaStaticFiles is but you can add cache options in UseStaticFiles. You have missed to set an Expires header.
// 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));
}
});
Beware that you also need a way to invalidate cache when you make changes to static files.
I have written a blog post about this: Minify and cache static files in ASP.NET Core

Related

Serving static index.html from the spa folder

I use .netcore server side and vuejs 2.
I have generated html files for some routes of my vuejs, that I placed directly in the dist folder:
I can access the html files with http://my-domain/en/home/index.html, but calling http://my-domain/en/home (without the index.html) won't serve the html file. Instead, it will return the equivalent spa page.
What can I do to fix this? I want the server to return the html file if it exists in priority, otherwise return the normal spa website.
Here is part of my startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// ...
// In production, the vue files will be served from this directory
services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/dist"; });
// ...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
// WebRootPath == null workaround. - from https://github.com/aspnet/Mvc/issues/6688
if (string.IsNullOrWhiteSpace(env.WebRootPath))
{
env.WebRootPath = Path.Combine(Directory.GetCurrentDirectory(), "ClientApp", "dist");
}
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
const int durationInSeconds = 60 * 60 * 24;
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
"public,max-age=" + durationInSeconds;
}
});
app.UseSpaStaticFiles();
// ...
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseVueCli(npmScript: "serve", port: 8085);
}
});
}
EDIT: On top of #Phil 's response, I needed to provide a FileProvider because UseDefaultFiles wasn't looking in the right folder:
app.UseDefaultFiles(new DefaultFilesOptions
{
FileProvider = new PhysicalFileProvider(env.WebRootPath) // important or it doesn't know where to look for
});
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
const int durationInSeconds = 60 * 60 * 24;
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
"public,max-age=" + durationInSeconds;
},
FileProvider = new PhysicalFileProvider(env.WebRootPath) // same as UseDefaultFiles
});
You need to tell the server to use Default Files
With UseDefaultFiles, requests to a folder search for:
default.htm
default.html
index.htm
index.html
app.UseDefaultFiles(); // this must come first
app.UseStaticFiles(...
This basically sets up an interceptor for requests on a folder (like your en/home) and if it finds any of the above filenames, will rewrite the URL to folder/path/{FoundFilename}.
If you want to avoid searches for anything other than index.html, you can customise it
DefaultFilesOptions options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("index.html");
app.UseDefaultFiles(options);
Note the important information about ordering
Important
UseDefaultFiles must be called before UseStaticFiles to serve the default file. UseDefaultFiles is a URL rewriter that doesn't actually serve the file. Enable Static File Middleware via UseStaticFiles to serve the file.

ASP.Net core 3.0 (3.1) set PathBase for single page application

I have the following asp.net core spa application configured (react-redux template)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UsePathBase(new PathString("/foo"));
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
I’d like to set pathBase for application, but app.UsePathBase(new PathString("/foo")) just ignored. On 2.2 it perfectly worked. Automatically modified index.html and all static files were moved to relative path. But on 3.0 (3.1) static + generated files are placed on root.
Generated files on .Net Core 2.2
Generated files on .Net Core 3.0
Does anyone have any ideas for solving it? Or may be some examples of Startup.cs with working pathBase?
Usually, app.UsePathBase(new PathString("/foo")); is used because the reverse proxy cuts off some prefix and causes ASP.NET Core app doesn't realize the virtual app path is start with /foo. In your scenario, if you don't have a reverse proxy that rewrite the prefix to empty string, you don't need app.UsePathBase(...).
Instead, if you your spa runs on a subpath, you could setup a middleware that branches the /foo.
Finally, you might want to add a property of homepage in your package.json so that it will generate the <base url="/foo/"/> when publishing. Or as an alternative, you could update the <base url=""> in ClientApp/public/index.html manually.
In short, add a "homepage": "/foo/" in your package.json
"private": true,
"homepage": "/foo/",
"dependencies": {
...
}
And setup a /foo branch to make SPA runs under that path:
string spaPath = "/foo";
app.Map(spaPath,appBuilder =>{
appBuilder.UseSpa(spa =>
{
spa.Options.DefaultPage = spaPath+"/index.html";
spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions{
RequestPath = spaPath,
};
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
});
Don't use app.UsePathBase(new PathString("/foo")) unless you understand you do want to override the path base for all routes.

Client Side Deep Links with WebpackDevMiddleware 404s

I am using the WebpackDevMiddleware for Development builds to serve up a Vue.js application that uses client-side routing. The SPA application is served up from the root url just fine, but if I attempt to use any client-side deep links, I get a 404.
Note running as Production works as expected.
What I want:
http://locahost/ - serve up the vue app.
http://localhost/overlays/chat - serve up the vue app.
http://localhost/api/* - serve up the api routes handled server side.
There is a minimum viable reproduction of the problem in this repository. You can run it using vscode debugging as Development environment where the bug happens. There is also a script /scripts/local-production that will build and run as Production environment, where it works as expected.
Relevant portions of my Startup.cs looks like this:
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// In production, the Vue files will be served
// from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = Configuration["Client"];
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//set up default mvc routing
app.UseMvc(routes =>
{
routes.MapRoute("default", "api/{controller=Home}/{action=Index}/{id?}");
});
//setup spa routing for both dev and prod
if (env.IsDevelopment())
{
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true,
ProjectPath = Path.Combine(env.ContentRootPath, Configuration["ClientProjectPath"]),
ConfigFile = Path.Combine(env.ContentRootPath, Configuration["ClientProjectConfigPath"])
});
}
else
{
app.UseWhen(context => !context.Request.Path.Value.StartsWith("/api"),
builder => {
app.UseSpaStaticFiles();
app.UseSpa(spa => {
spa.Options.DefaultPage = "/index.html";
});
app.UseMvc(routes => {
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Fallback", action = "Index" });
});
});
}
}
}
I was able to get around this using the status code pages middleware to handle all status codes and re-execute using the root path. This will cause the spa app to be served up for all status codes in the 400-599 range which is not quite what I want but gets me working again at least.
//setup spa routing for both dev and prod
if (env.IsDevelopment())
{
//force client side deep links to render the spa on 404s
app.UseStatusCodePagesWithReExecute("/");
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true,
ProjectPath = Path.Combine(env.ContentRootPath, Configuration["ClientProjectPath"]),
ConfigFile = Path.Combine(env.ContentRootPath, Configuration["ClientProjectConfigPath"])
});
}
Hopefully, this will help someone in the future that might be bumping up against this issue.

Unable to set HTTP header in asp.net core for the SPA files

Looking to set HTTP headers when serving files that are part of a SPA application in ASP.Net core 2.2 when running from the command line directly (using Kestrel).
Following the instructions at https://github.com/aspnet/AspNetCore/issues/3147#issuecomment-435617378, I can't get the StaticFileOptions.OnPrepareResponse events firing at all. Headers don't get set, and even breakpoints don't get triggered.
Statup.Configure() looks like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseSpaStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Add("Hello", "World"); // Never triggers
}
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Add("Hello", "World"); // Never triggers
}
};
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
Turns out that the events don't fire when running the angular dev server. These lines were the culprit:
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
Commenting it or changing the ASPNETCORE_ENVIRONMENT environment variable did the trick.

Not able to increase session timeout in ASP.NET Core 2.0

I am not able to increase the session timeout in ASP.NET Core 2.0. Session gets expired after every 20 to 30 minutes. When I decrease the time to 1 minutes and debug it it works fine but when it is increased to more more than 30 minutes or hours/days it does not last to specified duration.
Session is expired after 30 minutes in debug mode as well as from IIS (Hosted after publish).
options.IdleTimeout = TimeSpan.FromMinutes(180);
I am using below code in startup.cs file.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Adds a default in-memory implementation of IDistributedCache.
services.AddDistributedMemoryCache();
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.Expiration = TimeSpan.FromDays(3);
options.ExpireTimeSpan= TimeSpan.FromDays(3);
options.SlidingExpiration = true;
});
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromMinutes(180);
});
services.AddSingleton<IConfiguration>(Configuration);
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IConfigurationSettings configurationSettings)
{
//
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseSession();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseExceptionHandler(
builder =>
{
builder.Run(
async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
var ex = context.Features.Get<IExceptionHandlerFeature>();
if (ex != null)
{
if(ex.Error is SessionTimeOutException)
{
context.Response.StatusCode = 333;
}
if (ex.Error is UnauthorizedAccessException)
{
context.Response.StatusCode =999;
}
var err = JsonConvert.SerializeObject(new Error()
{
Stacktrace = ex.Error.StackTrace,
Message = ex.Error.Message
});
await context.Response.Body.WriteAsync(Encoding.ASCII.GetBytes(err), 0, err.Length).ConfigureAwait(false);
}
});
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
The issue might be coming because of your application pool get recycled on IIS. by default IIS Application pool get recycled after every 20 minutes.
Try to increase the application pool recycle time.
To change the application pool recycle time. Go through the following link
https://www.coreblox.com/blog/2014/12/iis7-application-pool-recycling-and-idle-time-out
I work with asp.net core 2.2 (selfhosting in kestrel, without IIS) and had the same problem with some session variables (they were reset after about 20-30 minutes).
In startup.cs, I had:
services.AddSession();
(without options, as I tought a session lives automatically as long as the user have a connection)
I have changed this to:
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(8);
});
And... it seems to work (in Kestrel (production) and also with IIS-Express (Debugging)).
Regarding recycling (as mentioned above), according to this posting:
[Kestrel recycling:][1]
Kestrel webserver for Asp.Net Core - does it recycle / reload after some time
It seems as Kestrel don’t recycle.