I would like to implement caching at the action level in MVC in a certain way.
I am aware of the OutputCache attribute, but I can't cache the entire page.
I would like to cache the model returned by the action.
So basically, I want to create a filter that will prevent the action method from being invoked, but have MVC behave as if it was invoked.
Assume that I plan on ignoring any "return View("viewName")" assuming all will be "return View()".
You can create a filter that inherits from ActionFilterAttribute
This is what I use
public class CacheControlAttribute : ActionFilterAttribute
{
public CacheControlAttribute(HttpCacheability cacheability)
{
_cacheability = cacheability;
}
private readonly HttpCacheability _cacheability;
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
cache.SetCacheability(_cacheability);
cache.SetExpires(DateTime.Now);
cache.SetAllowResponseInBrowserHistory(false);
cache.SetNoServerCaching();
cache.SetNoStore();
}
}
You can do partial caching.
For example, you can make an action method which IS NOT invoked as a regular action, but rather renders a partial view (an HTML snippet eventually) by calling Html.RenderPartial(). That way, you don't cache the whole page, but only those fragments which change less frequently.
Related
In MVC an OutputCacheAttribute is capable to prevent the execution of the action it is decorating (if the relative cache exists)
How to implement the same mechanism with a custom attribute?
In other works I would like to be able to decorate an action, and based on a logic internal to the attribute it will decide whether the action should carry on executing or not.
Additions
I've implemented a mechanism by which if a request to an action arrives with a querystring like flushaction=flush_silent, the custom attribute (which is extending the OutputCacheAttribute) invalidates the cache.
What I would also like to do, is not to execute the Action:
[JHOutputCache(CacheProfile = "ContentPageController.Index")]
public ActionResult Index(string url)
{
//custom code that should not execute when flushing the cache
}
As JHOutputCache extends OutputCacheAttribute, which derives from ActionFilterAttribute, halting execution of the underlying action is quite simple:
public class JHOutputCacheAttribute : OutputCacheAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (condition)
filterContext.Result = new EmptyResult();
else
base.OnActionExecuting(filterContext);
}
}
You can return any valid ActionResult here, including any custom ActionResult you may have derived.
I need to be able to process a querystring parameter throughout the site (like ?promo=38 for example). I was trying the procedure specified here Passing a {sitename} parameter to MVC controller actions but it wasn't working. My guess it's because according to http://blog.stevensanderson.com/2007/11/20/aspnet-mvc-pipeline-lifecycle/ querystring processing happens after the controller is instantiated.
So what would be a simple way to accomplish what I want? namely, being able to do something like setting a base controller property, or setting a session variable, from a querystring parameter anywhere in my site, without having to manually specify something in all the controller actions?
Override OnActionExecuting() your base controller or in an Action Filter as suggested by #jrummell. An Action Filter might be the way to go, but you would still need to decorate all of your controllers or create a base controller and decorate that with it.
public class PromoActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.Controller.ViewBag.Promo = filterContext.HttpContext.Request.QueryString("Promo");
base.OnActionExecuting(filterContext);
}
}
[PromoActionFilter]
public class BaseController : Controller
{
... Some Actions ...
}
some of my controller actions require a user to be authenticated. Those actions are flagged with a custom [Authorize] attribute. Behind the scene, a custom membership provider does some magic, among which setting some temporary data into the common-thread.
At the end of each action that required an authentication, a call to the OnActionExecuted() filter is required to cleanup the thread. This is done via another custom attribute called [CleanupContext].
So my actions look like this:
[Authorize]
[CleanupContext]
public ViewResult Action()
{
...
}
Since those two are always used together, since I am lazy and since I fear that someday one dev might forget to put one or the other and we end up with some weird behavior: is there a way to combine them into one attribute?
[AuthorizeAndCleanup]
public ViewResult Action()
{
// Aaah, if only it could look like this :D
}
Thanks a lot!
You could derive from AuthorizeAttribute in order to do your custom authorization stuff and implement IActionFilter in order to have access to the OnActionExecuting and OnActionExecuted events (to do your custom cleanup code):
public class AuthorizeAndCleanupAttribute : AuthorizeAttribute, IActionFilter
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
// TODO: your custom authorization logic
return base.AuthorizeCore(httpContext);
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
// TODO: your custom cleanup code
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
}
}
Obviously you should be aware that neither the OnActionExecuting or the OnActionExecuted events will ever be executed if the authorization fails (a.k.a. the AuthorizeCore method returns false) so make sure you do your cleanup in this method if you are about to return false.
A quick. dirty and (possibly) slow solution that I can think of is to skip the cleanup attribute and check for the presence of the custom Authorize attribute in the OnActionExecuted() and execute any cleanup code if you find it (since you stated that they are always present together).
You should implement your own filter provider (http://bradwilson.typepad.com/blog/2010/07/service-location-pt4-filters.html) which will automatically add Cleanup attribute to any action marked by Authorize.
So I have an MVC app that should change the Website title, and header color based on the domain the app is hit from. So I have a simple table setup in SQL as such:
DomainName (PK), WebsiteTitle, HeaderColor
Domain1.com, Website Title for Domain 1, #ebebeb
So I am trying to figure out the best way to return this information for each page view. Sure I can go ahead and lookup the site info in each model thats returned from the controller. But are there any other ways I can approach this? Maybe at a lower level in the stack?
Thank you!
There are many ways you can do this. ActionFilters are one way, or in a BaseController.
You need to determine if every action requires this, or if only certain actions.
If you decide every action, create a controller base, inheriting from Controller, then overriding OnActionExecuting. In that method you can make you calls to fetch and add the data to viewdata. Like so:
public class BaseController : Controller
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.Controller.ViewData.Add("SiteTitle", "Site title");
base.OnActionExecuting(filterContext);
}
}
If you prefer to use a base viewmodel that has this information, it would be best to override OnActionExectued where you can get access to the actions results, and modify the base model to set your values. Like so:
public class BaseController : Controller
{
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
var result = filterContext.Result as ViewResultBase;
var baseModel = (BaseViewModel) result.ViewData.Model;
baseModel.SiteTitle = "Site Title";
base.OnActionExecuted(filterContext);
}
}
Depending if you want an inheritence chain for your viewmodels. Either works. You'll also notice that I just set the values. Use whatever source for values you need. If you are pulling them from the db, I would cache the values so that for every action you are not hitting the db for it.
This problem is fundamentally identical to swapping layout or master pages for mobile vs desktop browsers. However, instead of looking at the device caps in a web request to determine which layout to use, you'd check the domain of the request.
See this article for a slightly complex (but thorough) overview of selecting mobile vs desktop views. Much of what the author says is focused on detecting screen solution, etc., which doesn't directly apply to you, but the mechanism for selecting the master or layout page should be just what you're looking for.
Or, you can handle this through inheritance.
Implement a base controller, like so:
public class BaseController : Controller
{
public string SiteTitle { get { .... } }
public string HeaderColor { get { ... } }
/// whatever other "global" properties you need
}
Then, each of your controllers inherit from BaseController
public class HomeController : BaseController
{
public ActionResult Index()
{
var myTitle = SiteTitle;
/// then, do whatever you want with it
return View();
}
}
In the property accessors in BaseController, read the title and whatever other properties you need from a .settings file or the AppSettings section in web.config.
Controller also provides events that can be used to set these properties so that you don't have to duplicate any code for getting those values into each view.
I started off using the default project's AccountController, but I've extended/changed it beyond recognition. However, in common with the original I have a LogOn and LogOff action.
Clearly, the LogOn action must be accessible to everyone. However, since I've added lots of other actions to this controller (to create & edit users), I want 99% of the actions to require administrator role membership.
I could decorate all my actions with [Authorize Roles="Administrators"] but there's a risk I'll forget one. I'd rather make it secure by default, by decorating the controller class itself with that attribute, and then relax the requirement on my LogOn method. Can I do that?
(As in, can I do that out-of-the-box without creating custom classes, etc. I don't want to complicate things more than necessary.)
To override an controller Attribute at the Action level you have to create a custom Attribute and then set the Order property of your custom attribute to a higher value than the controller AuthorizeAttribute. I believe both attributes are then still executed unless your custom attribute generates a result with immediate effect such as redirecting.
See Overriding controller AuthorizeAttribute for just one action for more information.
So I believe in your case you will just have to add the AuthorizeAttribute on the Actions and not at the controller level. You could however create a unit test to ensure that all Actions (apart from LogOn) have an AuthorizeAttribute
You can use AuthorizeAttribute on your class
http://msdn.microsoft.com/en-us/library/system.web.mvc.authorizeattribute.aspx
For relaxing you can implement for example a custom action filter attribute like this (I didn' test if it works).
public class GetRidOfAutorizationAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
// you can for example do nothing
filterContext.Result = new EmptyResult();
}
}
After way too much time, I came up with a solution.
public class OverridableAuthorize : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
var action = filterContext.ActionDescriptor;
if(action.IsDefined(typeof(IgnoreAuthorization), true)) return;
var controller = action.ControllerDescriptor;
if(controller.IsDefined(typeof(IgnoreAuthorization), true)) return;
base.OnAuthorization(filterContext);
}
}
Which can be paired with IgnoreAuthorization on an Action
public class IgnoreAuthorization : Attribute
{
}