可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have recently come across the Last-Modified Header.
- How and where can I include it in MVC?
- What are the advantages of including it?
I want an example how last modified header can be included in an mvc project, for static pages and database queries as well?
Is it different from outputcache, if yes how?
Basically, I want the browser to clear the cache and display the latest data or pages automatically, without the need for the user to do a refresh or clearing the cache.
回答1:
The Last-Modified
is mainly used for caching. It's sent back for resources for which you can track the modification time. The resources doesn't have to be files but anything. for instance pages which are generated from dB information where you have a UpdatedAt
column.
Its used in combination with the If-Modified-Since
header which each browser sends in the Request (if it has received a Last-Modified
header previously).
How and where can I include it in MVC?
Response.AddHeader
What are the advantages of including it?
Enable fine-grained caching for pages which are dynamically generated (for instance you can use your DB field UpdatedAt
as the last modified header).
Example
To make everything work you have to do something like this:
public class YourController : Controller
{
public ActionResult MyPage(string id)
{
var entity = _db.Get(id);
var headerValue = Request.Headers['If-Modified-Since'];
if (headerValue != null)
{
var modifiedSince = DateTime.Parse(headerValue).ToLocalTime();
if (modifiedSince >= entity.UpdatedAt)
{
return new HttpStatusCodeResult(304, "Page has not been modified");
}
}
// page has been changed.
// generate a view ...
// .. and set last modified in the date format specified in the HTTP rfc.
Response.AddHeader('Last-Modified', entity.UpdatedAt.ToUniversalTime().ToString("R"));
}
}
You might have to specify a format in the DateTime.Parse.
References:
- HTTP status codes
- HTTP headers
Disclamer: I do not know if ASP.NET/MVC3 supports that you manage Last-Modified
by yourself.
Update
You could create an extension method:
public static class CacheExtensions
{
public static bool IsModified(this Controller controller, DateTime updatedAt)
{
var headerValue = controller.Request.Headers['If-Modified-Since'];
if (headerValue != null)
{
var modifiedSince = DateTime.Parse(headerValue).ToLocalTime();
if (modifiedSince >= updatedAt)
{
return false;
}
}
return true;
}
public static ActionResult NotModified(this Controller controller)
{
return new HttpStatusCodeResult(304, "Page has not been modified");
}
}
And then use them like this:
public class YourController : Controller
{
public ActionResult MyPage(string id)
{
var entity = _db.Get(id);
if (!this.IsModified(entity.UpdatedAt))
return this.NotModified();
// page has been changed.
// generate a view ...
// .. and set last modified in the date format specified in the HTTP rfc.
Response.AddHeader('Last-Modified', entity.UpdatedAt.ToUniversalTime().ToString("R"));
}
}
回答2:
UPDATE: Check my new answer
How and where can I include it in MVC?
The built-in OutputCache
filter does the job for you and it uses those headers for caching.
The OuputCache
filter uses the Last-Modified
header when you set the Location
as Client
or ServerAndClient
.
[OutputCache(Duration = 60, Location = "Client")]
public ViewResult PleaseCacheMe()
{
return View();
}
What are the advantages of including it?
Leveraging client-side caching with conditional cache flush
I want an example how last modified header can be included in an mvc project, for static pages and database queries as well?
This link contains enough information to try out a sample. For static pages like html, images IIS will take care of setting/checking the Last-Modified
header and it uses the file's last modified date. For database queries you can go for setting the SqlDependency
in the OutputCache
.
Is it different for outputcache, if yes how? When do I need to include Last-Modified Header and when to use outputcache?
OutputCache
is an action filter used for implementing caching mechanism in ASP.NET MVC. There are different ways you could perform caching using OutputCache
: client-side caching, server-side caching. Last-Modified
header is one way to accomplish caching in the client-side. OutputCache
filter uses it when you set the Location
as Client
.
If you go for client-side caching (Last-Modified
or ETag
) the browser cache will automatically get updated in subsequent request and you don't need to do F5.
回答3:
Last-Modified vs. OutputCache
The OutputCache attribute controls output caching on your IIS WebServer. This is a vendor specific server feature (see Configure IIS 7 Output Caching).
I also suggest to read Cache Exploration in ASP.NET MVC3 if you are interested in the powerful capabilities of this technology.
Last-Modified response header and it's counterpart If-Modified-Since request header are representatives of the validation cache concept (section cache control).
These headers are part of the HTTP protocol and specified in rfc4229
OutputCache and validation are not exclusive, you can combine it.
What caching scenario makes me happy?
As usual: it depends.
Configuring a 5 second OutputCache on a 100 hits/second page would drastically reduce the load. Using OutputCache, 499 out of 500 hits can be served from cache (and do not cost db roundtrip, calculations, rendering).
When I have to serve rarely changes immediately, then the validation scenario could save a lot of bandwith. Specially when you serve large content compared to a lean 304 status message. However, changes are adopted immediately since every request validates for changes in the source.
Last-Modified attribute implementation sample
Based on my experience I would recommend to implement the validation scenario (last modified) as an action filter attribute.
(Btw: Here's an other caching scenario implemented as an attribute)
Static content from file
[LastModifiedCache]
public ActionResult Static()
{
return File("c:\data\static.html", "text/html");
}
Dynamic content sample
[LastModifiedCache]
public ActionResult Dynamic(int dynamicId)
{
// get data from your backend (db, cache ...)
var model = new DynamicModel{
Id = dynamivId,
LastModifiedDate = DateTime.Today
};
return View(model);
}
public interface ILastModifiedDate
{
DateTime LastModifiedDate { get; }
}
public class DynamicModel : ILastModifiedDate
{
public DateTime LastModifiedDate { get; set; }
}
The LastModifiedCache attribute
public class LastModifiedCacheAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.Result is FilePathResult)
{
// static content is served from file in my example
// the last file write time is taken as modification date
var result = (FilePathResult) filterContext.Result;
DateTime lastModify = new FileInfo(result.FileName).LastWriteTime;
if (!HasModification(filterContext.RequestContext, lastModify))
filterContext.Result = NotModified(filterContext.RequestContext, lastModify);
SetLastModifiedDate(filterContext.RequestContext, lastModify);
}
if (filterContext.Controller.ViewData.Model is HomeController.ILastModifiedDate)
{
// dynamic content assumes the ILastModifiedDate interface to be implemented in the model
var modifyInterface = (HomeController.ILastModifiedDate)filterContext.Controller.ViewData.Model;
DateTime lastModify = modifyInterface.LastModifiedDate;
if (!HasModification(filterContext.RequestContext, lastModify))
filterContext.Result = NotModified(filterContext.RequestContext, lastModify);
filterContext.RequestContext.HttpContext.Response.Cache.SetLastModified(lastModify);
}
base.OnActionExecuted(filterContext);
}
private static void SetLastModifiedDate(RequestContext requestContext, DateTime modificationDate)
{
requestContext.HttpContext.Response.Cache.SetLastModified(modificationDate);
}
private static bool HasModification(RequestContext context, DateTime modificationDate)
{
var headerValue = context.HttpContext.Request.Headers["If-Modified-Since"];
if (headerValue == null)
return true;
var modifiedSince = DateTime.Parse(headerValue).ToLocalTime();
return modifiedSince < modificationDate;
}
private static ActionResult NotModified(RequestContext response, DateTime lastModificationDate)
{
response.HttpContext.Response.Cache.SetLastModified(lastModificationDate);
return new HttpStatusCodeResult(304, "Page has not been modified");
}
}
How to enable global LastModified suppport
You can add the LastModifiedCache attribute to the RegisterGlobalFilters section of your global.asax.cs to globally enable this type of caching in your mvc project.
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
...
filters.Add(new LastModifiedCacheAttribute());
...
}
回答4:
Note that outputcache isn't your only option here, and in fact you may well not want to handle last-modified in the way it does. To clarify a few options:
Option 1 - use [OutputCache]
In this case the framework will cache the response body according to whatever duration you specify. It will serve it with Last-Modified set to the current time, and max-age set to the time remaining until the original cache duration expires. If a client sends a request with If-Modified-Since then the framework will correctly return a 304. Once the cached response expires then the Last-Modified date will be updated each time a new response is cached.
- Pros: caching happens at the controller level (so can work for partial content or the same cached content on different end URLs). You have better control over cacheability - e.g. HttpCacheability.ServerAndPrivate allows your server to cache the content, but not intermediate proxies.
- Cons: you have no control over last-modified. When your cache expires all clients will need to re-download the content even if it hasn't actually changed
Option 2 - specify settings on Response.Cache
asp.net has another layer of caching outside of the outputcacheattribute in the form of System.Web.OutputCacheModule which all requests pass through. This behaves like an HTTP cache immediately in front of your application. So if you set sensible cache headers without applying the OutputCacheAttribute then your response will be cached here instead. For example:
Response.Cache.SetLastModified(lastModifiedDate);
Response.Cache.SetCacheability(HttpCacheability.Public);
Response.Cache.SetExpires(DateTime.Now + timespan);
Based on the above then the outputcachemodule would cache your content and any requests to the same URL would be served from the cache. Requests with If-Modified-Since would get 304s. (You could use ETags equally). When your cache expires the next request will hit your app normally, but if you know the content hasn't changed you can return the same Last-Modified or ETag as you did previously. Once this next response has been cached then subsequent clients will then be able to extend their cache lifetimes without re-downloading the content
- Pros: If you have a meaningful way of determining last-modified or ETag then you have full control over it and can reduce the number of duplicate downloads.
- Cons: Caching is only at the request/URL level. Only works if you're happy to set cache-control: public
Although this option reduces the likelihood of unnecessary content downloads, it doesn't eliminate it - the first request after the (server) cache expires will be served normally and result in a 200, even if a 304 would have been appropriate. That's probably for the best though, since it enables the cache to grab a fresh copy of the response body which it would have discarded when it expired previously, and hence future requests can be served straight from cache. I believe that HTTP caches can in theory be cleverer than this and use 304s to extend their own cache lifetime, but the asp.net one appears not to support that.
(Edited to replace SetMaxAge with SetExpires in the code above - seems IIS/asp.net won't respect max-age headers unless you also SetSlidingExpiration(true) but that setting appears to prevent the caching we want)
回答5:
This is my second answer after doing some quite research on caching and OutputCache
.
Let me answer your second question first.
What are the advantages of including it?
Browser caches the responses returned from the server. The caching is controlled by mainly three headers: Cache-Control
, Last-Modified
and Expires
(there are others like ETag
also comes to play).
The Last-Modified
header tells the browser when does the resource has been modified at last. The resource could be either static file or dynamically created view. Whenever the browser makes the request for that resource it checks with the server "Hey, I already have a response for this request and it's Last-Modified
date is so and so.. see the user is already tired... if you return a 304 I'm glad to use the response from my cache else please send your new response quick". (Note that the browser passes the Last-Modified
value returned previously by the server in a new header called If-Modified-Since
)
Ideally the server should read the value from the If-Modified-Since
header and has to check with the current modified date and if they are same then it should return 304 (NOT MODIFIED) or it should return the new copy of the resource again passing the current modified date in the Last-Modified
header.
The advantage is browser caching. By leveraging the browser caching the server can avoid creating a duplicate response and also it can return a new response if the cached response in the browser looks like old. The ultimate goal is save the time.
How and where can I include it in MVC?
In the case of static resources like images, html files and others you don't need to worry about setting How and Where because IIS takes care of that job. IIS uses the file's last modified date as the Last-Modified
header value.
In the case of dynamic pages like a html content returned through an MVC action, how you can determine the Last-Modified
header value? The dynamic driven pages are mostly data driven and it's our responsibility to decide whether the response returned previously is stale or not.
Let's say you have a blog and you have a page whether you display the details of an article (not any other details) then the page's version is decided by the last modified date or created date (if the article is not modified yet) of the article. So you have to do the same work answered by @jgauffin in the corresponding action that delivers the view.
You have asked in the comment Should I include it per action in the controller?
If you could able to abstract away the logic of reading the last modified date from database from the actions then you could accomplish the job through an action filter avoiding duplicating the code throughout actions. The question is how you are going to abstract the details away from the actions? Like passing the table/column names to the attribute? You have to figure it out!
As an example..
[LastModifiedCacheFilter(Table = "tblArticles", Column = "last_modified")]
public ViewResult Post(int postId)
{
var post = ... get the post from database using the postId
return View(post);
}
The pseudo code (means I haven't tested this :) of the LastModifiedCacheFilterAttribute
implementation shown below uses Table/Column to read the last modified date but it could be some other ways as well. The idea is in the OnActionExecuting
method we are doing the check and returning a 304 (if the cache is still fresh) and in the OnResultExecuted
method we are reading/setting the latest modified date.
public class LastModifiedCacheFilterAttribute : ActionFilterAttribute
{
// Could be some other things instead of Table/Column
public string Table { get; set; }
public string Column { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// var lastModified = read the value from the passed Column/Table and set it here
var ifModifiedSinceHeader = filterContext.RequestContext.HttpContext.Request.Headers["If-Modified-Since"];
if (!String.IsNullOrEmpty(ifModifiedSinceHeader))
{
var modifiedSince = DateTime.Parse(ifModifiedSinceHeader).ToLocalTime();
if (modifiedSince >= lastModified)
{
filterContext.Result = new EmptyResult();
filterContext.RequestContext.HttpContext.Response.Cache.SetLastModified(lastModified.ToUniversalTime());
filterContext.RequestContext.HttpContext.Response.StatusCode = 304;
}
}
base.OnActionExecuting(filterContext);
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
// var lastModified = read the value from the passed Column/Table and set it herefilterContext.RequestContext.HttpContext.Response.Cache.SetLastModified(lastModified.ToUniversalTime());
base.OnResultExecuted(filterContext);
}
}
Why can't OutputCache attribute?
As per my analysis, OutputCache
attribute not uses Last-Modified
caching mechanism. The other thing is it uses the old page caching mechanism making it difficult to customize/extend.
Do you really need to implement the last-modified mechanism in all your actions?
Really not required. You could implement the last-modified mechanism to the actions that takes more time to create such a response and it takes more time to travel the response down the wire and reach the browser. In other cases I feel it just an overhead implementing throughout all the actions and also you have to measure out the benefits before doing so. The other main point is, in many cases the version of the page is not just decided by a single table column it could be by many other things and in those cases it may be more complicated to implement this!
A point about ETag
Though the question is about Last-Modified
header I should tell something about ETag
before clicking the Post Your Answer button. Compared to Last-Modified
(which relies on datetime) header ETag
header (relies on a hash value) is more accurate in determining whether the cached response in the browser is fresh or not but it could be little complicated to implement. IIS also includes ETag
header along with the Last-Modified
header for static resources. Before implementing any of this mechanism google out and see whether there is any library out there that helps you out!