We use a ActionFilterAttribute to inject some parameters into actions and it works great.
But when we add OutputCache it varies exclusively on "MyID" when Html.RenderAction() is used and not when surfing directly to the action.
Any ideas how to get OutputCache to always recognize "MyID"?
Controller
[SiteIDs, OutputCache]
public ActionResult SiteContent(string myID)
{
return Content(myID);
}
ActionFilter
public class SiteIDs : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.ActionParameters.ContainsKey("MyID"))
{
filterContext.ActionParameters["MyID"] = GetMyIDByHostname();
}
base.OnActionExecuting(filterContext);
}
}
With the OutputCacheAttribute, action filters will not execute when a page is retrieved from the cache. You should probably use mvcdonutcaching in order to have the action filters executed even when retrieving from the cache. I'd recommend reading this.
Option 1
According to this answer, you just need to use VaryByParam = "*" and it will automatically vary by the parameters you pass the action method.
[SiteIDs, OutputCache(VaryByParam = "*")]
public ActionResult SiteContent(string myID)
{
return Content(myID);
}
However, that may not work by using an IActionFilter (haven't tried it). You might try using an IValueProvider instead (which is a cleaner way to do what you are doing with the action filter, anyway).
Option 2
You could use VaryByCustom and GetVaryByCustomString to vary the cache by hostname.
[SiteIDs, OutputCache(VaryByParam = "none", VaryByCustom = "hostname")]
public ActionResult SiteContent(string myID)
{
return Content(myID);
}
And in your Global.asax file:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();
}
public override string GetVaryByCustomString(HttpContext context, string custom)
{
if (custom == "hostname")
{
return "HostName=" + context.Request.Url.Host;
}
return base.GetVaryByCustomString(context, custom);
}
}
Keep in mind your action filter will only be hit if the OutputCache has not been set. So you need to vary the cache on the same value (or values) that you vary your ID from. The simplest solution is to use something that is already in HttpContext, such as host name.
Related
Would someone tell me if there is an analog of Response.AddHeader("Refresh", "10") in ASP. NET MVC5, please?
I have tried [OutputCache(NoStore = true, Location = OutputCacheLocation.Client, Duration = 10)] but it does not work.
[OutputCache] is for, well, caching the output of an action. The Duration param merely tells it how long to cache that output. Neither has anything to do with setting HTTP headers, and certainly will not make a page refresh automatically.
Reponse.AddHeader is still valid in MVC5; you just need to ensure that you have not started the response yet. Unless you're doing something off-the-wall, that's not difficult. If you're returning a ViewResult, for example, just call this first:
Response.AddHeader("Refresh", "10");
return View();
If you're directly writing to the response, then just ensure you add the header before you start doing that.
You can use it directly in your controller
public ActionResult MyAction()
{
Response.AddHeader("Refresh", "10");
return View();
}
Or you can make a custom action filter
public class RefreshAttribute : ActionFilterAttribute, IActionFilter
{
public string Duration { get; set; }
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var duration = 10;
Int32.TryParse(this.Duration, out duration);
filterContext.HttpContext.Response.AddHeader("Refresh", duration.ToString());
}
}
Usage
[Refresh(Duration = "10")]
public ActionResult MyAction()
{
return View();
}
I'm using azure mobile app and i notice that each attribute i put in a controller is not getting honored. only global filters works.
for example
[MobileAppController]
[ZboxAuthorize]
public class BoxesController : ApiController
{
[ZboxAuthorize]
[Route("api/boxes", Order = 3)]
public async Task<HttpResponseMessage> GetBoxesAsync()
{
var userid = User.GetCloudentsUserId();
return Request.CreateResponse("ok");
}
now zboxauthorize attribute is not working. the ctor get called but nothing else
the atuthorize attribute looks like this
public class ZboxAuthorizeAttribute : AuthorizeAttribute, IOverrideFilter
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
return false;
}
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
HttpContext.Current.Response.AddHeader("AuthenticationStatus", "NotAuthorized");
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Forbidden);
}
if i put it in my global filter its works perfectly.
what am i missing? thanks.
I have an extremely odd error and wondered if anyone knew the reason for this.
When I create a new DataObject and TableController called Content and ContentController respectively, it doesn't register the tablecontroller and the help documentation it automatically generates has lost its styling.
I can't connect to the controller at all but all other controllers work as expected.
If I just rename it to DataController and that's just the name of the controller, not the dataobject everything works perfectly.
Is ContentController a reserved word of some kind or is this just specifically happening on my machine?
public class DataController : TableController<Content>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
MobileContext context = new MobileContext();
DomainManager = new EntityDomainManager<Content>(context, Request, Services);
}
// GET tables/Content
public IQueryable<Content> GetAllContent()
{
return Query();
}
// GET tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public SingleResult<Content> GetContent(string id)
{
return Lookup(id);
}
// PATCH tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task<Content> PatchContent(string id, Delta<Content> patch)
{
return UpdateAsync(id, patch);
}
// POST tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public async Task<IHttpActionResult> PostContent(Content item)
{
Content current = await InsertAsync(item);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
// DELETE tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task DeleteContent(string id)
{
return DeleteAsync(id);
}
}
An MVC project will create an application directory called Content. This will override your route mapping to the ContentController.
You can get around this if desired through changing RouteMaps and other trickery although probably the simpliest answer is to change the name of the controller...
I've got this action in MVC
[OutputCache(Duration = 1200, VaryByParam = "*")]
public ActionResult FilterArea( string listType, List<int> designersID, int currPage = 1 )
{
// Code removed
}
that fails to present the correct HTML with url like
http://example.com/en-US/women/clothing?designersID=158
http://example.com/en-US/women/clothing?designersID=158&designersID=13
Is this a know bug of OutputCache in .NET cause cannot recognize VaryByParam with a list param or am I missing something?
I too had the same issue in MVC3 and I believe it's still the same case in MVC5.
Here is the setup I had.
Request
POST, Content-Type:application/json, passing in an array of string as the parameter
{ "options": ["option1", "option2"] }
Controller Method
[OutputCache(Duration = 3600, Location = OutputCacheLocation.Any, VaryByParam = "options")]
public ActionResult GetOptionValues(List<string> options)
I tried every option possible with OutputCache and it just wasn't caching for me. Binding worked fine for the actual method to work. My biggest suspicion was that OutputCache wasn't creating unique cache keys so I even pulled its code out of System.Web.MVC.OutputCache to verify. I've verified that it properly builds unique keys even when a List<string> is passed in. Something else is buggy in there but wasn't worth spending more effort.
OutputCacheAttribute.GetUniqueIdFromActionParameters(filterContext,
OutputCacheAttribute.SplitVaryByParam(this.VaryByParam);
Workaround
I ended up creating my own OutputCache attribute following another SO post. Much easier to use and I can go enjoy the rest of the day.
Controller Method
[MyOutputCache(Duration=3600)]
public ActionResult GetOptionValues(Options options)
Custom Request class
I've inherited from List<string> so I can call the overriden .ToString() method in MyOutputcache class to give me a unique cache key string. This approach alone has resolved similar issues for others but not for me.
[DataContract(Name = "Options", Namespace = "")]
public class Options: List<string>
{
public override string ToString()
{
var optionsString= new StringBuilder();
foreach (var option in this)
{
optionsString.Append(option);
}
return optionsString.ToString();
}
}
Custom OutputCache class
public class MyOutputCache : ActionFilterAttribute
{
private string _cachedKey;
public int Duration { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.Url != null)
{
var path = filterContext.HttpContext.Request.Url.PathAndQuery;
var attributeNames = filterContext.ActionParameters["Options"] as AttributeNames;
if (attributeNames != null) _cachedKey = "MYOUTPUTCACHE:" + path + attributeNames;
}
if (filterContext.HttpContext.Cache[_cachedKey] != null)
{
filterContext.Result = (ActionResult) filterContext.HttpContext.Cache[_cachedKey];
}
else
{
base.OnActionExecuting(filterContext);
}
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
filterContext.HttpContext.Cache.Add(_cachedKey, filterContext.Result, null,
DateTime.Now.AddSeconds(Duration), System.Web.Caching.Cache.NoSlidingExpiration,
System.Web.Caching.CacheItemPriority.Default, null);
base.OnActionExecuted(filterContext);
}
}
With MVC4 I was able to create and register a global action filter that would check the model state prior to the action's execution and return the serialized ModelState before any damage could be done.
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
However, with MVC5, I am having trouble finding Request and therefore CreateErrorResponse
public override void OnActionExecuting(ActionExecutingContext nActionExecutingContext)
{
if (!nActionExecutingContext.Controller.ViewData.ModelState.IsValid)
{
nActionExecutingContext.Result = // Where is Request.CreateErrorResponse ?
}
}
I realize that I could create a custom response class to assign to Result but I'd rather use what's built-in if CreateErrorResponse is still available.
Any idea where I can find it relative to an ActionExecutingContext in MVC5 / Web API 2?
I know this is an old question but I recently had the same problem and solved it using
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}