Outputcache 1 action, 2 views - asp.net-mvc

So I have the following action which I am trying to add output caching to:
[OutputCache(CacheProfile = OutputCacheProfileNames.Hours24)]
public ActionResult ContactUs()
{
ContactUsModel model = _modelBuilder.BuildContactUsModel();
if (Request.IsAjaxRequest())
{
return Json(StringFromPartial(partialTemplate, model), JsonRequestBehavior.AllowGet);
}
else
{
return View(model);
}
}
But this seem to cache the first view that is requested - ie either the json OR the normal view.
Is there a way to get the output caching to work for both views, without having to split them out of the same action?

You beat me to the punch in answering your own question, but I thought this code may still be helpful. Since varying by user is such a common scenario, you should probably account for being able to do that and your AJAX vary. This code will allow you vary on any number of custom parameters, by appending to a single string to vary on.
public override string GetVaryByCustomString(System.Web.HttpContext context, string custom)
{
var args = custom.ToLower().Split(';');
var sb = new StringBuilder();
foreach (var arg in args)
{
switch (arg)
{
case "user":
sb.Append(User.Identity.Name);
break;
case "ajax":
if (context.Request.Headers["X-Requested-With"] != null)
{
// "XMLHttpRequest" will be appended if it's an AJAX request
sb.Append(context.Request.Headers["X-Requested-With"]);
}
break;
default:
continue;
}
}
return sb.ToString();
}
Then, you would just do something like the following if you need to vary by multiple custom params.
[OutputCache(CacheProfile = OutputCacheProfileNames.Hours24, VaryByCustom = "User;Ajax")]
Then, if you ever need additional custom vary params, you just keep adding case statements to cover those scenarios.

Thanks to the comments by REDEVI_ for pointing me in the right direction, I have been able to solve this.
I changed my output caching to:
[OutputCache(CacheProfile = OutputCacheProfileNames.Hours24, VaryByCustom = "IsAjax")]
And then in my global.asax file, I added the following override:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
if (context != null)
{
switch (custom)
{
case "IsAjax":
return new HttpRequestWrapper(context.Request).IsAjaxRequest() ? "IsAjax" : "IsNotAjax";
}
}
return base.GetVaryByCustomString(context, custom);
}

Related

How might I simultaneously bind FromQuery and FromRoute parameter?

I am needing to simultaneously support a query-parameter based route (/api/models?id=1) and a route based one (/api/models/1) while still allowing unambiguous access to the models collection (/api/models)?
My controller looks (something) like this:
[Route("/api/{controller}")]
public class ModelsController : Controller
{
[HttpGet]
public Models[] GetModels([FromQuery]QueryOptions queryOptions)
{
//...
}
[HttpGet("{id:int}")]
public Model Get([FromRoute] int id)
{
//...
}
[HttpGet("?{id:int}")]
public Model Get2Try1([FromQuery] int id)
{
//Fails with ": The literal section '?' is invalid.
//Literal sections cannot contain the '?' character."
//Which makes sense after some reading...
}
[HttpGet]
public Model Get2Try2([FromQuery] int id)
{
//Fails with "AmbiguousActionException: Multiple actions matched.
//The following actions matched route data and had all constraints satisfied:
//GetModels and Get2Try2"
//Which I think I understand as well...the absence of optional params
//means ambiguous routing...
}
[HttpGet] //What here?
public Model Get2Try3([FromQuery] int id) //and/or here?
{
}
}
I feel like there should be some way to (with declarative routing) accomplish this. Has anyone done anything along these lines?
Also, current code base is ASP.NET Core (RC1) to be upgraded to RTM/1.0 shortly. Details on either side are likely similar, but am interested in either/both.
I've found that the following works:
[HttpGet, Route("{id?}")]
... the key being mainly the '?'. You don't need any [FromX] in the method signature, this does the trick and caters for both query string and route parameter passing.
Unfortunately Swagger UI doesn't like it and expects some explicit parameter to work out of the box (https://github.com/domaindrivendev/Ahoy/issues/47 or https://github.com/domaindrivendev/Ahoy/issues/182), but that's another story :)
I had the same problem.
There aren't solutions that works (against wep api .net) with web api core.
If we set [Route("{id}")] and [Route("")] doesn't work; if we set only [Route("{id?}")] the query parameter is empty if I use querystring.
So, I've used a workround.
I used [Route("{id?}")], but inside the function I get param from Request.Query
Example
public T Cast<T>(string input)
{
T output = default(T);
if (string.IsNullOrWhiteSpace(input))
return output;
input = input.Trim();
try
{
Type typeToCastTo = typeof(T);
if (typeof(T).IsGenericType)
typeToCastTo = typeToCastTo.GenericTypeArguments[0];
if (typeToCastTo.IsEnum)
{
if (Enum.IsDefined(typeToCastTo, input))
return (T)Enum.Parse(typeToCastTo, input);
return output;
}
object value = Convert.ChangeType(input, typeToCastTo, CultureInfo.InvariantCulture);
return (value == null) ? output : (T)value;
}
catch
{
return output;
}
}
public void MapQuerystringParams<T>(ref T param, string name)
{
var q = Request.Query[name].FirstOrDefault();
if (q != null)
{
var cast = Cast<T>(q);
if (!cast.Equals(default(T)))
param = cast;
}
}
[Route("api/[controller]/[action]")]
[ApiController]
public class ActivityController : ControllerBase
{
//examples of call
//https://localhost:44345/api/Activity/GetActivityByCode/7000
//https://localhost:44345/api/Activity/GetActivityByCode/?Id=7000
[HttpGet]
[Route("{Id?}")]
public IActionResult GetActivityByCode(int Id)
{
MapQuerystringParams(ref Id, "Id"); //this take param from querystring if exists
ActivityBusiness business = new ActivityBusiness(new BusinessInitializer { config = configuration });
ActivityDTOModel activity = business.GetActivityByCode(Id);
return Ok(activity);
}
}
Ideally in the domain design if you can have one method serving one specific function then great. Recently I had to faithfully implement a legacy API and it wasn't an option for me to decompose the design of my API.
If you are suffering from ambiguous routes in MVC6 and need to differentiate unique Routes given specific QueryStrings that have been supplied at one single POST method. Then IActionConstraint can help! Here is some example code of me using it :
using System;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
namespace Automation.Api.Service.Attributes
{
public class RoutingSpecificAttribute : Attribute, IActionConstraint
{
private string _keyParam;
public RoutingSpecificAttribute(string routingParameter)
{
this._keyParam = routingParameter;
}
public int Order
{
get
{
return 0;
}
}
public bool Accept(ActionConstraintContext context)
{
if (this._keyParam == null) { return true; }
switch (this._keyParam)
{
case "name": return context.RouteContext.HttpContext.Request.Query.ContainsKey(this._keyParam);
case "noquerystring": return context.RouteContext.HttpContext.Request.Query.Count == 0;
default:
return false;
}
}
}
}
This one method in the API that I needed to author both serviced a separate create + update functions based on the existence of a couple of QueryStrings: name & version.
So to help disambiguate you can distinctly decorate each of the methods within your controllers within said controller class [RoutingSpecific("noquerystring")] or [RoutingSpecific("name")] to help differentiate.
MSDN class description
Example implementation - see Entropy github
For anyone that happens to stumble upon this as I have,
Using .Net Core 3.1 the following works:
Web Controller Method
[HttpGet("something/{id}")]
public IActionResult Get([FromRoute] id, [FromQuery] OptionalParams optionalParams)
{
// do stuff
}
Query Parameter Container
public class OptionalParams
{
[FromQuery(Name = "colour_of_thing")]
public string Colour { get; set; }
[FromQuery(Name = "shape_of_thing")]
public string Shape { get; set; }
[FromQuery(Name = "some_other_filter")]
public string SomeOtherFilter { get; set; }
}
Url
var id = Guid.NewGuid();
var colour = "red";
var shape = "circle";
var url = $"Http://localhost:5000/something/{id}?colour_of_thing={colour}&shape_of_thing={shape}";

MVC Session global variable

Here is what I'm trying to achieve. Certain options at the navbar should be available only if the user has "subordinates" in the database.
So, at the navbar I have:
The Approvals should be hidden for some users, but available to others. For those whom it should be available, the user must:
A) Be a Supervisor or,
B) Have a subornidate at the DB table
So, as for "A" it's pretty straightforward. I did:
#if (User.IsInRole("Supervisor"))
{
<li>#Html.ActionLink("Approvals", "Index", "Approval")</li>
}
For "B", I was suggested to use Sessions. Well, great. So I came to the question: how can I make a single request to the DB and assign it to a Session["HasSubordinates"] so I can do this check?
#if (User.IsInRole("Supervisor") || (bool)Session["HasSubordinates"])
{
<li>#Html.ActionLink("Approvals", "Index", "Approval")</li>
}
What I tried was to have:
Session["HasSubordinates"] = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
for every single controller, but that didn't worked well because sometimes I get null pointer and it looks absolutely rubbish.
I know it may sound like a trivial question for some (or most), but I'm really stuck and I do really appreciate any help.
Looking at your code, getting a user subordinates should only happen once. In your Login method:
Session["HasSubordinates"] = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
Create a new class to extend IPrincipal:
public class IPrincipalExtensions
{
public bool HasSubordinates(this IPrincipal user)
{
return Session != null && Session["HasSubordinates"] != null && Session["HasSubordinates"] > 0;
}
}
Now, in the View:
#if (User.IsInRole("Supervisor") || User.HasSubordinates() )
{
}
Writing from memory, may have left something out, but this should be the cleanest.
Don't use the session for this. What you need is a child action.
[ChildActionOnly]
public ActionResult Nav()
{
var model = new NavViewModel
{
IsSupervisor = User.IsInRole("Supervisor");
HasSubordinates = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
}
return ParialView("_Nav", model);
}
Then, just create a partial view, _Nav.cshtml and utilize the properties on the view model to render your nav however you like.
If you want, you can even use output caching on the child action, so it's only evaluated once per user. There's no built-in way to vary the cache by user, so first, you'll need to override the following method in Global.asax:
public override string GetVaryByCustomString(System.Web.HttpContext context, string custom)
{
var args = custom.ToLower().Split(';');
var sb = new StringBuilder();
foreach (var arg in args)
{
switch (arg)
{
case "user":
sb.Append(User.Identity.Name);
break;
case "ajax":
if (context.Request.Headers["X-Requested-With"] != null)
{
// "XMLHttpRequest" will be appended if it's an AJAX request
sb.Append(context.Request.Headers["X-Requested-With"]);
}
break;
default:
continue;
}
}
return sb.ToString();
}
With that, you can then just decorate your child action with:
[OutputCache(Duration = 3600, VaryByCustom = "User")]

asp.net mvc - return view doesn't fire the code... url wrong

I have following code that is accepting a form submission
[ActionName("TestingTemp"), AcceptVerbs(HttpVerbs.Post)]
public ActionResult TestingTemp(FormCollection result)
{
string cat = "";
return View("Try");
}
Now the problem is even though it seems to load "Try" page, things break on the page because it doesn't fire the following code (which does get properly fired if I directly go to Try page).
public ActionResult Try()
{
ViewData["Test"] = DataLayer.Test(0, 10);
return View();
}
Also the url contains TestingTemp where it should contain Try, if you know what I mean.
I think what you are looking for is RedirectToAction. It will redirect to your other method and rewrite the URL.
[ActionName("TestingTemp"), AcceptVerbs(HttpVerbs.Post)]
public ActionResult TestingTemp(FormCollection result)
{
string cat = "";
return RedirectToAction("Try");
}
the preferred way is to use redirectToAction but if u do want to go that way then u have to put the required data that u r doing in Try method like
[ActionName("TestingTemp"), AcceptVerbs(HttpVerbs.Post)]
public ActionResult TestingTemp(FormCollection result)
{
string cat = "";
ViewData["Test"] = DataLayer.Test(0, 10);
return View("Try");
}
but as i said this way is not preferred i.e repeating ur code in each action rather u can just write something like
[ActionName("TestingTemp"), AcceptVerbs(HttpVerbs.Post)]
public ActionResult TestingTemp(FormCollection result)
{
string cat = "";
return RedirectToAction("Try");
}

Is there a way to maintain IsAjaxRequest() across RedirectToAction?

If you don't want any context or an example of why I need this, then skip to The question(s) at the bottom!
In a bid to keep things tidy I initially built my application without JavaScript. I am now attempting to add a layer of unobtrusive JavaScript on the top of it.
In the spirit of MVC I took advantage of the easy routing and re-routing you can do with things like RedirectToAction().
Suppose I have the following URL to kick off the sign up process:
http://www.mysite.com/signup
And suppose the sign up process is two steps long:
http://www.mysite.com/signup/1
http://www.mysite.com/signup/2
And suppose I want, if JavaScript is enabled, the sign up form to appear in a dialog box like ThickBox.
If the user leaves the sign up process at step 2, but later clicks the "sign up" button, I want this URL:
http://www.mysite.com/signup
To perform some business logic, checking the session. If they left a previous sign up effort half way through then I want to prompt them to resume that or start over.
I might end up with the following methods:
public ActionResult SignUp(int? step)
{
if(!step.HasValue)
{
if((bool)Session["SignUpInProgress"] == true)
{
return RedirectToAction("WouldYouLikeToResume");
}
else
{
step = 1;
}
}
...
}
public ActionResult WouldYouLikeToResume()
{
if(Request.IsAjaxRequest())
{
return View("WouldYouLikeToResumeControl");
}
return View();
}
The logic in WouldYouLikeToResume being:
If it's an AJAX request, only return the user control, or "partial", so that the modal popup box does not contain the master page.
Otherwise return the normal view
This fails, however, because once I redirect out of SignUp, IsAjaxRequest() becomes false.
Obviously there are very easy ways to fix this particular redirect, but I'd like to maintain the knowledge of the Ajax request globally to resolve this issue across my site.
The question(s):
ASP.NET MVC is very, very extensible.
Is it possible to intercept calls to RedirectToAction and inject something like "isAjaxRequest" in the parameters?
OR
Is there some other way I can detect, safely, that the originating call was an AJAX one?
OR
Am I going about this the completely wrong way?
As requested by #joshcomley, an automated answer using the TempData approach:
This assumes that you have a BaseController and your controllers are inheriting from it.
public class AjaxianController : /*Base?*/Controller
{
private const string AjaxTempKey = "__isAjax";
public bool IsAjax
{
get { return Request.IsAjaxRequest() || (TempData.ContainsKey(AjaxTempKey)); }
}
protected override RedirectResult Redirect(string url)
{
ensureAjaxFlag();
return base.Redirect(url);
}
protected override RedirectToRouteResult RedirectToAction(string actionName, string controllerName, System.Web.Routing.RouteValueDictionary routeValues)
{
ensureAjaxFlag();
return base.RedirectToAction(actionName, controllerName, routeValues);
}
protected override RedirectToRouteResult RedirectToRoute(string routeName, System.Web.Routing.RouteValueDictionary routeValues)
{
ensureAjaxFlag();
return base.RedirectToRoute(routeName, routeValues);
}
private void ensureAjaxFlag()
{
if (IsAjax)
TempData[AjaxTempKey] = true;
else if (TempData.ContainsKey(AjaxTempKey))
TempData.Remove(AjaxTempKey);
}
}
To use this, make your controller inherit from AjaxianController and use the "IsAjax" property instead of the IsAjaxRequest extension method, then all redirects on the controller will automatically maintain the ajax-or-not flag.
...
Havn't tested it though, so be wary of bugs :-)
...
Another generic approach that doesn't require using state that I can think of may requires you to modify your routes.
Specifically, you need to be able to add a generic word into your route, i.e.
{controller}/{action}/{format}.{ajax}.html
And then instead of checking for TempData, you'd check for RouteData["ajax"] instead.
And on the extension points, instead of setting the TempData key, you add "ajax" to your RouteData instead.
See this question on multiple format route for more info.
This worked for me.
Please note that this doesn't require any session state which is a potential concurrency issue:
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
if (this.Request.IsAjaxRequest)
{
if (filterContext.Result is RedirectToRouteResult)
{
RedirectToRouteResult rrr = (RedirectToRouteResult)filterContext.Result;
rrr.RouteValues.Add("X-Requested-With",Request.Params["X-Requested-With"]);
}
}
}
}
Perhaps you can add a AjaxRedirected key in the TempData property before doing the redirection?
One way to transfer state is to add an extra route parameter i.e.
public ActionResult WouldYouLikeToResume(bool isAjax)
{
if(isAjax || Request.IsAjaxRequest())
{
return PartialView("WouldYouLikeToResumeControl");
}
return View();
}
and then in the Signup method:
return RedirectToAction("WouldYouLikeToResume", new { isAjax = Request.IsAjaxRequest() });
// Don't forget to also set the "ajax" parameter to false in your RouteTable
// So normal views is not considered Ajax
Then in your RouteTable, default the "ajax" parameter to false.
Or another way to go would be override extension points in your BaseController (you do have one, right?) to always pass along the IsAjaxRequest state.
..
The TempData approaches are valid too, but I'm a little allergic of states when doing anything that looks RESTful :-)
Havn't tested/prettify the route though but you should get the idea.
I would just like to offer what I believe is a MUCH better answer than the current accepted one.
Use this:
public class BaseController : Controller
{
private string _headerValue = "X-Requested-With";
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
var ajaxHeader = TempData[_headerValue] as string;
if (!Request.IsAjaxRequest() && ajaxHeader != null)
Request.Headers.Add(_headerValue, ajaxHeader);
}
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
if (Request.IsAjaxRequest() && IsRedirectResult(filterContext.Result))
TempData[_headerValue] = Request.Headers[_headerValue];
}
private bool IsRedirectResult(ActionResult result)
{
return result.GetType().Name.ToLower().Contains("redirect");
}
}
Then make all your controllers inherit from this.
What it does:
Before an action executes this checks to see if there is a value in TempData. If there is then it manually adds its value to the Request object's header collection.
After an action executes it checks if the result was a redirect. If it was a redirect and the request was an Ajax Request before this action was hit then it reads the value of the custom ajax header that was sent and stores it in temp data.
This is better because of two things.
It is shorter and cleaner.
It adds the request header to the Request object after reading the temp data. This allows Request.IsAjaxRequest() to work normally. No calling a custom IsAjax property.
Credit to: queen3 for his question containing this solution. I did modify it to clean it up a bit but it is his solution originally.
The Problem is in the Client-Cache.
To overcome this, just add a cachebreaker
like "?_=XXXXXX" to Location Url in the 302 Response.
Here is my working Filter. Regisiter it in the GlobalFilter Collection.
I added the Location Header to the Redirected Response, so the client script can get the destination url, in the ajax call. (for Google-Analytics)
public class PNetAjaxFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.HttpContext.Request;
if(request.QueryString["_"] == "ajax")
{
filterContext.HttpContext.Request.Headers["X-Requested-With"] = "XMLHttpRequest";
request.QueryString.Remove("_");
}
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
//public override void OnResultExecuting(ResultExecutingContext filterContext)
{
var context = filterContext.HttpContext;
if (!context.Request.IsAjaxRequest())
return;
var request = context.Request;
String noCacheQuery = String.Empty;
if (request.HttpMethod == "GET")
{
noCacheQuery = request.QueryString["_"];
}
else if (context.Response.IsRequestBeingRedirected)
{
var pragma = request.Headers["Pragma"] ?? String.Empty;
if (pragma.StartsWith("no-cache", StringComparison.OrdinalIgnoreCase))
{
noCacheQuery = DateTime.Now.ToUnixTimestamp().ToString();
}
else
{
//mode switch: one spezial cache For AjaxResponse
noCacheQuery = "ajax";
}
}
if (!String.IsNullOrEmpty(noCacheQuery))
{
if (context.Response.IsRequestBeingRedirected)
{
var location = context.Response.RedirectLocation;
if (location.Contains('?'))
location += "&_=" + noCacheQuery;
else
location += "?_=" + noCacheQuery;
context.Response.RedirectLocation = location;
}
else
{
var url = new UriBuilder(request.Url);
if (url.Port == 80 && url.Scheme == Uri.UriSchemeHttp)
url.Port = -1;
else if(url.Port == 443 && url.Scheme == Uri.UriSchemeHttps)
url.Port = -1;
if(!String.IsNullOrEmpty(url.Query))
url.Query = String.Join("&", url.Query.Substring(1).Split('&').Where(s => !s.StartsWith("_=")));
context.Response.AppendHeader("Location", url.ToString());
}
}
}
}
And here the jQuery:
var $form = $("form");
var action = $form.attr("action");
var $item = $("body");
$.ajax({
type: "POST",
url: action,
data: $form.serialize(),
success: function (data, status, xhr) {
$item.html(data);
var source = xhr.getResponseHeader('Location');
if (source == null) //if no redirect
source = action;
$(document).trigger("partialLoaded", { source: source, item: $item });
}
});

ASP.NET MVC - Reusing Action Behaviors

This question pertains primarily to good design.
Suppose I have a controller action like DeletePage that can be invoked in two separate views of the same controller. Assuming the delete logic is not contained in the action itself, but rather some conditional checks and the like that call the correct business logic, it doesn't make sense to duplicate the structure of the delete action when I can instead have a private method that returns an ActionResult which I call in both actions which can cause a delete. My question is where is the best place to place a reusable action method like this? Right now I'm just marking them private and sticking them in a region of the controller class, but perhaps an sealed inner class would make more sense for such a method- or somewhere else entirely.
Thoughts?
public ActionResult EditPage(int id, FormCollection formCollection)
{
var page = _pagesRepository.GetPage(id);
if (page == null)
return View("NotFound");
if (page.IsProtected)
return View("IllegalOperation");
if (formCollection["btnSave"] != null)
{
//...
}
else if (formCollection["btnDelete"] != null)
{
return DeletePage(page);
}
return RedirectToAction("Index");
}
public ActionResult DeletePage(int id)
{
var page = _pagesRepository.GetPage(id);
if (page == null)
return View("NotFound");
return DeletePage(page);
}
// Reusable Action
private RedirectToRouteResult DeletePage(Page page)
{
if(page != null && !page.IsProtected)
{
_pagesRepository.Delete(page);
_pagesRepository.Save();
FlashMessage(string.Format(PageForms.PageDeleted, page.Name), MessageType.Success);
return RedirectToAction("Index");
}
return RedirectToAction("Index");
}
I don't see why you need to make your reusable method an action method. Why not just a private method that returns void/bool/etc indicating the result of the save, and let your public action method return the RedirectToAction()? Effectively it's the same result, but I think it's a clearer approach.
public ActionResult DeletePage(int id)
{
var page = _pagesRepository.GetPage(id);
if (page == null)
return View("NotFound");
DeletePage(page);
return RedirectToAction("Index");
}
//reusable method
private void DeletePage(Page page)
{
//your same validation/save logic here
}
In the future you might consider moving this private DeletePage method into a separate service class that performs the validation and/or saving. Returning an an ActionResult would definitely not make sense in that case, so I think this example would be a more appropriate approach for your scenario.
In my opinion, your reusable code is an Action because it is returning an ActionResult. So your use is fine. The DeletePage(Page page) could potentially remain public.
I look forward to other opinions.
Personally I agree with Kurt. The concept of Deleting an unprotected page should be decoupled from what action the controller should perform. Secondly it's confusing from the code what should happen when the page is protected. In one action it redirects to the index, in the second it redirects to the "IllegalOperation" view. Personally I'd do something a little like...
public ActionResult DeletePage(int id) {
var page = _pagesRepository.GetPage(id);
if (!PageIsValidForDeletion(page)) {
string invalidPageView = FindViewForInvalidPage(page);
return View(invalidPageView);
}
DeletePage(page);
return RedirectToAction("Index");
}

Resources