Choose OutputCache policy based on query string - asp.net-mvc

I have an ASP.NET MVC application where certain resources are addressed like this:
/controller/action/id?revision=123
The revision parameter is optional:
if it is missing I do a 302 redirect to the latest revision. I want this redirection response to be cached only for a short while, or not at all.
if it is present, I want to cache the response for a long time because any given revision of the resource is immutable.
My first attempt was to do something like this:
[OutputCache(Duration=10,Location=OutputCacheLocation.Server)]
public Action(string id)
{
long lastRevision = GetLastRevision(id);
return RedirectToAction("Action",
new { Id = id, revision = lastRevision });
}
[OutputCache(Duration=int.MaxValue,Location=OutputCacheLocation.Server)]
public Action(string id, long revision)
{
// ...
}
Unfortunately, the ASP.NET MVC routing doesn't seem to like method overloads. It expects to have a single Action method with an optional parameter instead (i.e. long? revision), but then I can't specify different caching policies for both cases.
How can I chose a different caching policy based on the presence of the query string here?

You could write a custom method selector:
public class RevisionMethodSelectorAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
var revision = controllerContext.Controller.ValueProvider.GetValue("revision");
var hasRevisionParam = methodInfo.GetParameters().Any(p => string.Equals("revision", p.Name, StringComparison.OrdinalIgnoreCase));
if (revision != null && !string.IsNullOrEmpty(revision.AttemptedValue) && hasRevisionParam)
{
return true;
}
if ((revision == null || string.IsNullOrEmpty(revision.AttemptedValue)) && !hasRevisionParam)
{
return true;
}
return false;
}
}
and then decorate the 2 actions with it:
[RevisionMethodSelector]
public ActionResult MyAction(string id)
{
long lastRevision = GetLastRevision(id);
return RedirectToAction("MyAction", new { id = id, revision = lastRevision });
}
[RevisionMethodSelector]
[OutputCache(Duration = int.MaxValue, Location = OutputCacheLocation.Server, VaryByParam = "revision")]
public ActionResult MyAction(string id, long revision)
{
...
}
The first action is not cached. It will be picked up if there's no revision parameter in the request and it will simply redirect to the second action. The second action is cached for a very long time, this cache is made to vary according to the revision parameter value (which you didn't have) and will be picked by the custom method selector if a revision parameter is present in the request.

It turns out that I had already solved this problem without realizing it by making use of 302 redirects: apparently 302 responses are not cached even if you have an OutputCache attribute on your controller method!
Therefore both cases can be handled by a single controller method with the [OutputCache(...)] attribute specifying what to do for 200 responses.
Though this now begs the question of what to do if you do want to cache a 302...

Related

In ASP.Net Web API, how do I map multiple http query parameters to a single method parameter

We're using ASP.Net Web API to generate a feed and it includes the ability to do paging.
myfeed.com/afeed?page=2
My boss says "let's also allow users to use 'paged', because that's what WP uses." In addition, we're also using pageIndex in some of our older feeds. So what I'd like to do is accept all three.
myfeed.com/afeed?page=2
myfeed.com/afeed?paged=2
myfeed.com/afeed?pageIndex=2
I'd like to do is be able to write a clean Web API method, such as
public Foo Get(int page = 1)
{
//do some stuff
return foo;
}
without cluttering the method with page 'plumbing'. So I tried creating an ActionFilter
public override void OnActionExecuting(HttpActionContext actionContext)
{
object pageParam = new object(); //query["page"]
if (pageParam == null)
{
var altPageParam = GetPageParamUsingAlternateParams(actionContext);
if (altPageParam != null){}
//SetPageParam here
}
base.OnActionExecuting(actionContext);
}
private object GetPageParamUsingAlternateParams(HttpActionContext actionContext)
{
object result = new object();
object pageIndexParam = new object(); //Query["pageIndex"]
object pagedParam = new object(); ////Query["paged"]
if (pagedParam != null)
result = pagedParam;
else if (pageIndexParam != null)
result = pageIndexParam;
return result;
}
I didn't finish. As I was looking for the best way to get the query params, I stumbled into a big mistake!
OnActionExecuting is executed after int page = 1. Sure, I could override it in an ActionFilter, but that would lead to confusion down the road. I really want to be able to do a simple flow through the URI query parameters that goes from
page -> paged -> pageIndex -> default value in method
I have found a lot of articles on custom binding to a an object. Also, I found articles about "parameter binding", however those dealt with FromUri and FromBody. I didn't find anything that I felt had a direct parallel to what I'm facing.
You could achieve what you want by defining 3 different GET method with parameters matched with the query segment of the Url like the code snippet below:
public class ProductsController : ApiController
{
//Matched api/products?page=1
public IHttpActionResult Get(int page)
{
return GetPagedData(page);
}
//Matched api/products?paged=1
public IHttpActionResult GetPaged(int paged)
{
return GetPagedData(paged);
}
//Matched api/products?pagIndex=1
public IHttpActionResult GetPageIndex(int pageIndex)
{
return GetPagedData(pageIndex);
}
//Do the real paging here
private IHttpActionResult GetPagedData(int page =1)
{
return Ok("Data Pages");
}
}

Web API - Only 1 Action Per HTTP Verb/Parameter Combination?

I'm quite familiar with MVC, but fairly new to WebAPI and I've run into a confusing issue.
I have a controller (Which inherits from ApiController) called "DummyController" and it's got the 5 default scaffolded methods for get,post,put and delete (2 for get), and I've addd my own method at the bottom called "FindDummyObjects()" which I've decorated with the [HttpGet] attribute.
when I navigate to "api/dummy" or "api/dummy/get", I get the default result fo the 2 string objects ("value1" and "value2").
However, when I navigate to "api/dummy/FindDummyObjects", it complains that "The parameters dictionary contains a null entry for parameter 'id'".
This means that it's not pointing to my Action at all (As it is parameterless), so it's probably pointing to the default "Get(int id)" action.
When I comment out all actions except my own, I get the results I expect.
So my question is this, with WebAPI, is it only possible to have 1 action per http verb with a certain set of parameters, regardless of whether the action's names differ?
For example, it seems as though it will not be possible for me to 10 different http GET actions in a single controller, unless they all have different parameters and use the action name "Get" (Unless I do some custom routing I suppose).
Is that correct?
Code:
// GET api/dummy
public IEnumerable Get()
{
return new string[] { "value1", "value2" };
}
// GET api/dummy/5
public string Get(int id)
{
return "value";
}
// POST api/dummy
public void Post([FromBody]string value)
{
}
// PUT api/dummy/5
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/dummy/5
public void Delete(int id)
{
}
[HttpGet]
public IEnumerable<Models.DummyObject> FindDummyObjects()
{
IList<Models.DummyObject> myDummyList = new List<Models.DummyObject>();
for (int i = 0; i < 10; i++)
{
Models.DummyObject dumObj = new Models.DummyObject();
dumObj.ObjectId = i;
dumObj.ObjectName = string.Empty;
myDummyList.Add(dumObj);
}
return myDummyList;
}
Web API routing has quite a few holes (I'm being polite), you happened to hit on one. This is one of the reasons that they introduced Attribute Routing in Web API2. You might want to try that as it is quite a bit more flexible.

Route Parameter, Custom Model Binder or Action Filter?

Our ASP.NET MVC application allows an authenticated user to administer one or more "sites" linked to their account.
Our Urls are highly guessible since we use the site friendly name in the URL rather than the Id e.g:
/sites/mysite/
/sites/mysite/settings
/sites/mysite/blog/posts
/sites/mysite/pages/create
As you can see we need access to the site name in a number of routes.
We need to execute the same behaviour for all of these actions:
Look for a site with the given identifier on the current account
If the site returned is null, return a 404 (or custom view)
If the site is NOT null (valid) we can carry on executing the action
The current account is always available to us via an ISiteContext object. Here is how I might achieve all of the above using a normal route parameter and performing the query directly within my action:
private readonly ISiteContext siteContext;
private readonly IRepository<Site> siteRepository;
public SitesController(ISiteContext siteContext, IRepository<Site> siteRepository)
{
this.siteContext = siteContext;
this.siteRepository = siteRepository;
}
[HttpGet]
public ActionResult Details(string id)
{
var site =
siteRepository.Get(
s => s.Account == siteContext.Account && s.SystemName == id
);
if (site == null)
return HttpNotFound();
return Content("Viewing details for site " + site.Name);
}
This isn't too bad, but I'm going to need to do this on 20 or so action methods so want to keep things as DRY as possible.
I haven't done much with custom model binders so I wonder if this is a job better suited for them. A key requirement is that I can inject my dependencies into the model binder (for ISiteContext and IRepository - I can fall back to DependencyResolver if necessary).
Many thanks,
Ben
Update
Below is the working code, using both a custom model binder and action filter. I'm still not sure how I feel about this because
Should I be hitting my database from a modelbinder
I can actually do both the retrieving of the object and null validation from within an action filter. Which is better?
Model Binder:
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (!controllerContext.RouteData.Values.ContainsKey("siteid"))
return null;
var siteId = controllerContext.RouteData.GetRequiredString("siteid");
var site =
siteRepository.Get(
s => s.Account == siteContext.Account && s.SystemName == siteId
);
return site;
}
Action Filter:
public class ValidateSiteAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var site = filterContext.ActionParameters["site"];
if (site == null || site.GetType() != typeof(Site))
filterContext.Result = new HttpNotFoundResult();
base.OnActionExecuting(filterContext);
}
}
Controller Actions:
[HttpGet]
[ValidateSite]
public ActionResult Settings(Site site)
{
var blog = site.GetFeature<BlogFeature>();
var settings = settingsProvider.GetSettings<BlogSettings>(blog.Id);
return View(settings);
}
[HttpPost]
[ValidateSite]
[UnitOfWork]
public ActionResult Settings(Site site, BlogSettings settings)
{
if (ModelState.IsValid)
{
var blog = site.GetFeature<BlogFeature>();
settingsProvider.SaveSettings(settings, blog.Id);
return RedirectToAction("Settings");
}
return View(settings);
}
This definitely sounds like a job for an action filter. You can do DI with action filters not a problem.
So yeah, just turn your existing functionality into a action filter and then apply that to each action OR controller OR a base controller that you inherit from.
I don't quite know how your site works but you could possibly use a global action filter that checks for the existence of a particular route value, e.g. 'SiteName'. If that route value exists, that means you need to follow through with checking that the site exists...
A custom model binder for your Site type sounds like a good idea to me.
You will probably also want an action filter as well to catch "null" and return not found.

Does VaryByParam="*" also read RouteData.Values?

in my asp.net mvc project, I enable output caching on a controller as below
[OutputCache(Duration = 100, VaryByParam = "*", VaryByHeader = "X-Requested-With")]
public class CatalogController : BaseController
{
public ActionResult Index(string seller)
{
// I do something
}
}
it works great, until create my own Route class as below
public class MyRoute : Route
{
// there is a constructor here..
// I override this method..
// just to add one data called 'seller' to RouteData
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var data = base.GetRouteData(httpContext);
if (data == null) return null;
var seller = DoSomeMagicHere();
// add seller
data.Values.Add("seller", seller);
return data;
}
}
and then, the action method will take seller as parameter. I tested it by always providing different seller parameter, but it take the output from cache instead of calling the method.
does setting VaryByParam="*" also vary by RouteData.Values, in asp.net mvc?
I'm using ASP.Net 4 MVC 3 RC 2
The output caching mechanism varies by URL, QueryString, and Form. RouteData.Values is not represented here. The reason for this is that the output caching module runs before Routing, so when the second request comes in and the output caching module is looking for a matching cache entry, it doesn't even have a RouteData object to inspect.
Normally this isn't a problem, as RouteData.Values comes straight from the URL, which is already accounted for. If you want to vary by some custom value, use VaryByCustom and GetVaryByCustomString to accomplish this.
If you remove VaryByParam = "*" it should use your action method parameter values when caching.
ASP.NET MVC 3’s output caching system no longer requires you to
specify a VaryByParam property when declaring an [OutputCache]
attribute on a Controller action method. MVC3 now automatically
varies the output cached entries when you have explicit parameters on your action method – allowing you to cleanly enable output...
Source: http://weblogs.asp.net/scottgu/archive/2010/12/10/announcing-asp-net-mvc-3-release-candidate-2.aspx
[OutputCache(Duration = 100, VaryByHeader = "X-Requested-With")]
public class CatalogController : BaseController
{
public ActionResult Index(string seller)
{
// I do something
}
}

OutputCache behavior in ASP.NET MVC 3

I was just testing Output Caching in the RC build of ASP.NET MVC 3.
Somehow, it is not honoring the VaryByParam property (or rather, I am not sure I understand what is going on):
public ActionResult View(UserViewCommand command) {
Here, UserViewCommand has a property called slug which is used to look up a User from the database.
This is my OutputCache declaration:
[HttpGet, OutputCache(Duration = 2000, VaryByParam = "None")]
However, when I try and hit the Action method using different 'slug' values (by manupulating the URL), instead of serving wrong data (which I am trying to force by design), it is instead invoking the action method.
So for example (in order of invocation)
/user/view/abc -> Invokes action method with slug = abc
/user/view/abc -> Action method not invoked
/user/view/xyz -> Invokes action method again with slug = xyz! Was it not supposed to come out of the cache because VaryByParam = none?
Also, what is the recommended way of OutputCaching in such a situation? (example above)
Just wanted to add this information so that people searching are helped:
The OutputCache behavior has been changed to be 'as expected' in the latest release (ASP.NET MVC 3 RC 2):
http://weblogs.asp.net/scottgu/archive/2010/12/10/announcing-asp-net-mvc-3-release-candidate-2.aspx
Way to go ASP.NET MVC team (and Master Gu)! You all are awesome!
VaryByParam only works when the values of the url look like /user/view?slug=abc. The params must be a QueryString parameter and not part of the url like your above examples. The reason for this is most likely because Caching happens before any url mapping and that mapping isn't included in the cache.
Update
The following code will get you where you want to go. It doesn't take into account stuff like Authorized filters or anything but it will cache based on controller/action/ids but if you set ignore="slug" it will ignore that particular attribute
public class ActionOutputCacheAttribute : ActionFilterAttribute {
public ActionOutputCacheAttribute(int cacheDuration, string ignore) {
this.cacheDuration = cacheDuration;
this.ignore = ignore;
}
private int cacheDuration;
private string cacheKey;
private string ignore;
public override void OnActionExecuting(ActionExecutingContext filterContext) {
string url = filterContext.HttpContext.Request.Url.PathAndQuery;
this.cacheKey = ComputeCacheKey(filterContext);
if (filterContext.HttpContext.Cache[this.cacheKey] != null) {
//Setting the result prevents the action itself to be executed
filterContext.Result =
(ActionResult)filterContext.HttpContext.Cache[this.cacheKey];
}
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext) {
//Add the ActionResult to cache
filterContext.HttpContext.Cache.Add(this.cacheKey, filterContext.Result,null, DateTime.Now.AddSeconds(cacheDuration),
System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
//Add a value in order to know the last time it was cached.
filterContext.Controller.ViewData["CachedStamp"] = DateTime.Now;
base.OnActionExecuted(filterContext);
}
private string ComputeCacheKey(ActionExecutingContext filterContext) {
var keyBuilder = new StringBuilder();
keyBuilder.Append(filterContext.ActionDescriptor.ControllerDescriptor.ControllerName);
keyBuilder.Append(filterContext.ActionDescriptor.ActionName);
foreach (var pair in filterContext.RouteData.Values) {
if (pair.Key != ignore)
keyBuilder.AppendFormat("rd{0}_{1}_", pair.Key.GetHashCode(), pair.Value.GetHashCode());
}
return keyBuilder.ToString();
}
}

Resources