I want to get the last modified time of any part of a view prior to it rendering. This includes layout pages, partial views etc.
I want to set a proper time for
Response.Cache.SetLastModified(viewLastWriteUtcTime);
to properly handle http caching. Currently I have this working for the view itself however if there are any changes in the layout pages, or child partial views those are not picked up by
var viewLastWriteUtcTime = System.IO.File.GetLastWriteTime(
Server.MapPath(
(ViewEngines.Engines.FindView(ControllerContext, ViewBag.HttpMethod, null)
.View as BuildManagerCompiledView)
.ViewPath)).ToUniversalTime();
Is there any way I can get the overall last modified time?
I don't want to respond with 304 Not Modified
after deployments that modified a related part of the view as users would get inconsistent behavior.
I'm not going to guarantee that this is the most effective way to do it, but I've tested it and it works. You might need to adjust the GetRequestKey() logic and you may want to choose an alternate temporary storage location depending on your scenario. I didn't implement any caching for file times since that seemed like something you wouldn't be interested in. It wouldn't be hard to add if it was ok to have the times be a small amount off and you wanted to avoid the file access overhead on every request.
First, extend RazorViewEngine with a view engine that tracks the greatest last modified time for all the views rendered during this request. We do this by storing the latest time in the session keyed by session id and request timestamp. You could just as easily do this with any other view engine.
public class CacheFriendlyRazorViewEngine : RazorViewEngine
{
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
UpdateLatestTime(controllerContext, GetLastModifiedForPath(controllerContext, viewPath));
var pathToMaster = masterPath;
if (string.IsNullOrEmpty(pathToMaster))
{
pathToMaster = "~/Views/Shared/_Layout.cshtml"; // TODO: derive from _ViewStart.cshtml
}
UpdateLatestTime(controllerContext, GetLastModifiedForPath(controllerContext, pathToMaster));
return base.CreateView(controllerContext, viewPath, masterPath);
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
UpdateLatestTime(controllerContext, GetLastModifiedForPath(controllerContext, partialPath));
return base.CreatePartialView(controllerContext, partialPath);
}
private DateTime GetLastModifiedForPath(ControllerContext controllerContext, string path)
{
return System.IO.File.GetLastWriteTime(controllerContext.HttpContext.Server.MapPath(path)).ToUniversalTime();
}
public static void ClearLatestTime(ControllerContext controllerContext)
{
var key = GetRequestKey(controllerContext.HttpContext);
controllerContext.HttpContext.Session.Remove(key);
}
public static DateTime GetLatestTime(ControllerContext controllerContext, bool clear = false)
{
var key = GetRequestKey(controllerContext.HttpContext);
var timestamp = GetLatestTime(controllerContext, key);
if (clear)
{
ClearLatestTime(controllerContext);
}
return timestamp;
}
private static DateTime GetLatestTime(ControllerContext controllerContext, string key)
{
return controllerContext.HttpContext.Session[key] as DateTime? ?? DateTime.MinValue;
}
private void UpdateLatestTime(ControllerContext controllerContext, DateTime timestamp)
{
var key = GetRequestKey(controllerContext.HttpContext);
var currentTimeStamp = GetLatestTime(controllerContext, key);
if (timestamp > currentTimeStamp)
{
controllerContext.HttpContext.Session[key] = timestamp;
}
}
private static string GetRequestKey(HttpContextBase context)
{
return string.Format("{0}-{1}", context.Session.SessionID, context.Timestamp);
}
}
Next, replace the existing engine(s) with your new one in global.asax.cs
protected void Application_Start()
{
System.Web.Mvc.ViewEngines.Engines.Clear();
System.Web.Mvc.ViewEngines.Engines.Add(new ViewEngines.CacheFriendlyRazorViewEngine());
...
}
Finally, in some global filter or on a per-controller basis add an OnResultExecuted. Note, I believe OnResultExecuted in the controller runs after the response has been sent, so I think you must use a filter. My testing indicates this to be true.
Also, note that I am clearing the value out of the session after it is used to keep from polluting the session with the timestamps. You might want to keep it in the Cache and set a short expiration on it so you don't have to explicitly clean things out or if your session isn't kept in memory to avoid the transaction costs of storing it in the session.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class UpdateLastModifiedFromViewsAttribute : ActionFilterAttribute
{
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
var cache = filterContext.HttpContext.Response.Cache;
cache.SetLastModified(CacheFriendlyRazorViewEngine.GetLatestTime(filterContext.Controller.ControllerContext, true));
}
}
Finally, apply the filter to the controller you want to use it on or as a global filter:
[UpdateLastModifiedFromViews]
public class HomeController : Controller
{
...
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With