Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC3 Get Last Modified time of any part of view?

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.

like image 322
Chris Marisic Avatar asked Jun 13 '12 14:06

Chris Marisic


1 Answers

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
{
    ...
}
like image 192
tvanfosson Avatar answered Sep 28 '22 04:09

tvanfosson