Output caching, VaryByParam/VaryByCustom - c#

OK, there seem to be no end to the number of articles that sing the praises of output caching and how much it will speed up your website. I have now read hundreds of articles and Q&As on the topic, but I still can't seem to make it work. (I think output caching might be stealing my soul)
My requirements are pretty simple, I have 3 pages that I would like to cache based on parameters: Home, Results, and Details. I also have a small area of the page that needs to be varied by user. I also need to cache to a central repository and I have chosen redis to hold my data. I should also mention that this is still an old web forms app.
My original approach was to attempt to supply my own custom string using the "VaryByCustom" option. The Microsoft page seems to make this look simple:
https://msdn.microsoft.com/en-us/library/5ecf4420.aspx
This requires placing an overridden method "GetVaryByCustomString" in the global.asax file. The problem is that most of the examples show using variables from HttpContext that are always available (Browser, Browser Version etc.) and even though Microsoft and others seem to suggest this as the preferred "cache by custom string" method I can't find any working examples that allow the page to define the string. I have seen a few examples of people using session and claiming that it works (session is null for me) and context.user (some people say that user is null) but I don't see any practical way to deliver a string except by using Response.Cache.SetVaryByCustom("my_custom_string"). After a day of strugling with implementation in the main project I decided to build an isolated project for testing/proof of concept. In this project I was able to get my custom string working as long as I passed it as the string to SetVaryByCustom. The problem is that this doesn't really match any examples I've seen. The examples show "SomeKey" with "SomeValue" being returned by GetVaryByCustomString. What worked for me was essentially "SomeValue" with "SomeValue". Here was my code:
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if (arg != null)
{
return arg;
}
return base.GetVaryByCustomString(context, arg);
}
This worked the first time on the page, after setting the value of a dropdown which created a postback, but never after that. So the "OnselectedItemChanged" fired once and created a new set of cache entries based on the selection, but it never fired again. I tried modifying just about every cache parameter I could to make it work but no matter what series of settings I attempted (set cache declaratively, set caching in code, tried various combinations of VaryByParams, adding location, etc) I was never successful.
After attempting for 2 days to get this to work I decided it was time for a different approach. I have gone back to a more traditional/accepted approach.
I created 3 cache profiles in my web.config:
<caching>
<cache disableExpiration="false" />
<outputCache defaultProvider="RedisOutputCacheProvider" enableOutputCache="true" enableFragmentCache="true" sendCacheControlHeader="true">
<providers>
<add name="RedisOutputCacheProvider"
type="LAC.OutputCacheProvider, LAC"
connectionString="192.168.XX.XX:6379,connectTimeout=10000"
dbNumber="5"
keyPrefix="LAC:OutputCache:" />
</providers>
</outputCache>
<outputCacheSettings>
<outputCacheProfiles>
<add name="Home" varyByParam="*" varyByCustom="user" duration="86400"/>
<add name="Results" varyByParam="*" varyByCustom="user" duration="7200" />
<add name="Details" varyByParam="*" varyByCustom="user" duration="7200"/>
</outputCacheProfiles>
</outputCacheSettings>
</caching>
So each page has a custom vary by user string that should help me cache a new page for an authenticated user vs an anonymous one.
Then I added a GetVaryByCustomString that looks like:
public override string GetVaryByCustomString(HttpContext context, string arg)
{
switch (arg)
{
case "user":
var identity = context.User.Identity.Name != "" ? context.User.Identity.Name : "Anonymous";
return string.Format("IsAuthenticated={0},Identity={1}", context.Request.IsAuthenticated, identity);
}
return string.Empty;
}
I am as close as I have ever been to having this working but it's still not 100%. The Home page works fine, the details page works fine, but the results page never caches. The results page has a filter containing 12 different dropdown lists. So it needs to store version of the page based on selections. I added a Timestamp to the top of all of my pages so I could see if the page was caching. The time on the results page changes every time I hit control-F5, which is not true on the other 2 pages. No cache entries are ever created and GetVaryByCustomString is never called. I have triple checked the code for some sort of "Turn cacheability Off" setting but as far as I can tell there is no code anywhere in the page that disables the cache. My global.asax file inherits from the correct class, my custom ouputcache provider seems to be adding the correct entries for the 2 pages that work. Of course now with VaryByParam="*" the key can be as long as the content thanks to viewstate (which has to be left on) BLECH!
So essentially I have caching set on three pages that are similar and one doesn't work. I have no idea where to look next. I am hoping someone like #kevinmontrose who really has a handle on this sort of stuff will take pity on me and give me a shove in a direction that will lead me to a solution.
UPDATE:
I have opened a case with Microsoft.

have you been setting VaryByParam? i forget the details at the moment but i recall getting stuck before finding out that it is mandatory. (in every case i think.(?)) i think duration is also required. (i'm referring to directives in the .aspx page.)

rereading your question i saw the issue: "I can't find any working examples that allow the page to define the string." i was rereading some articles and i'm pretty sure that's the wrong approach.
i haven't tested all of this but for ex, if you take the standard example:
<%# OutputCache Duration="10" VaryByParam="None" VaryByCustom="minorversion" %>
what seems to be happening is that you have just set the string to look for, and that's it.
when a request comes in, you check the HTTP request header - the Vary header, which is what you set in the page - and if it exists you do what you want.
<%# Application language="C#" %>
<script runat="server">
public override string GetVaryByCustomString(HttpContext context,
string arg)
{
// arg is your custom 'Vary' header.
if(arg == "minorversion")
{
return "Version=" +
context.Request.Browser.MinorVersion.ToString();
}
return base.GetVaryByCustomString(context, arg);
}
</script>
i just did a quick test of the basic example and added a label that is updated on page load with the current time. i refreshed the live page several times and the time did not change until 10 seconds had passed. so it definitely works. i think the key is that you set the value and you're done, but you can work with it if you want to.
... on second thought, i think you create the header in the page, and return the value from the global.asax. where?? i think it's sent to the caching system which decides if a new or cached page should be sent. in the example above the browser's minor version is checked and set and since there is a cached version available (after first load) it sends that, until 10 secs are up.
the docs for VaryByCustom is in a section for 'caching different versions of a page' so that's what this is doing. if someone with a different browser shows up, the browser version is checked - then the caching system checks to see if a version of the page for that browser version is available. if not, create a new page; if so, send the cached version.

Related

ASP.NET MVC 5 TempData and Session gets cleared on RedirectToAction [duplicate]

I have an interesting problem with the TempData object not passing values to another controller.
I set TempData["Enroll"] in the Enroll Controller's HttpPost method to an Enroll Model. I then read the TempData["Enroll"] object in the Register Controller's HttpGet method, but is empty/null.
I need to persist all of this data across 3 controllers.
Any thoughts?
Here is a code Snippet
//EnrollController.cs
[HttpPost]
public ActionResult Index(EnrollModel model)
{
// ...
TempData["EnrollModel"] = model;
return RedirectToAction("Index", "Register");
}
// RegisterController.cs
public ActionResult Index(string type)
{
RegisterModel model = new RegisterModel();
EnrollModel enrollModel = TempData["EnrollModel"] as EnrollModel;
model.ClientType = enrollModel.ClientType;
// ...
}
I've had an issue where TempData got lost during the redirect on my local machine.
I've checked web.config sessionState Setting which was InProc and therefore no problem.
It turned out that I got another setting in web.config, which was taken from production system. It looked like this:
<httpCookies requireSSL="true" />
After turning the requireSSL to false TempData workes fine.
I had the same problem today.
In this link some guys explain that RedirectAction method returns a HTTP 302 status to the browser, which causes the browser to make a new request and clear the temp, but I tried returning HTTP methods 303 (which is what the RedirectAction should be returning) and 307 also, and it didn't solve anything.
The only way of fixing the issue of TempData in my case was changing the sessionState directive of web.config to use StateServer instead of the default InProc. i.e:
<system.web>
<sessionState mode="StateServer" cookieless="AutoDetect" timeout="30" stateConnectionString="tcpip=localhost:42424"></sessionState>
...
</system.web>
I figured this out when reading this Greg Shackles' article, where he explains how TempData works and build a custom TempDataProvider, which rely on MongoDB database instead of session like the default one.
Hope that my 4 hours researching helps someone to not waste their time.
I have come across these sorts of limitations with TempData before. I found it unrealiable and sporadic at best.
You need to consider what you are trying to achieve. If you do need to store data, in practice the best place to do this is in a db (or store of sorts) it might seem a bit overkill but that is their purpose.
Two other points:
Someone can hit your RegisterController Index method without going to the other before, in which case your code would break.
If you are doing a multiple wizard style process, why not store the data in its partial state in the db, and complete the process only on the last screen? In this way no matter, where they stop/start or pick it up again, you will always know where they are in the process.
Save your results to a cache or db or pass in as posts/querystrings between your controllers. TempData is cleared by several things including a worker process reset which could surely happen between steps.
In addition your code above could get a null ref exception:
EnrollModel enrollModel = TempData["EnrollModel"] as EnrollModel;
if(enrollModel==null)
{
//handle this model being null
}
model.ClientType = enrollModel.ClientType;
Fixing your issue as you have it above though is tough without seeing all code and knowing if there is anything else that may/may not refer to it.

MVC configurable Authorization filter

I want to make the [MyAuthorize(Role="R1")] attribute so that
"R1" can be made configurable instead of hardcoding on Controller / Action.
The usual approach of creating a [MyAuthorize(Role="R1")] seems to be
public class MyAuthorizeAttribute : AuthorizeAttribute
{
private readonly string[] _allowedRoles;
public MyAuthorizeAttribute(params string[] roles)
{
this._allowedRoles = roles;
}
protected override bool OnAuthorization(AuthorizationContext
authorizationContext)
{
bool authorize = false;
// Compare current user's Roles with "R1" to figure out if the
// Action / Controller can be executed
return authorize;
}
}
But what if Roles like "R1" are subject to change at any time ?
i.e., being "R1" one day and being called "AssistantManager" another day.
The application will have to be re-coded to handle this.
I thought of creating a custom [OnAuthorize] attribute that reads
(Action/Controller, Role) as key value pairs from the web.config.
Eg:--
<add key="Controller1" value="Role1" />
<add key="Action2" value="Role2" />
and in the attribute..
protected override bool OnAuthorization(AuthorizationContext
authorizationContext)
{
bool authorize = false;
// 1. Read all key values
// 2. determine Action / Controller the user is trying to go
// 3. Compare user's roles with those for Action / Controller
return authorize;
}
I am aware of the limitations of <location .... /> in MVC
as per https://stackoverflow.com/a/11765196/807246
and I'm not suggesting that, even though I'm reading from web.config
But what if we read (..and store in session??) all the authorization related configuration when the application first loads up?
Any changes like "R1" -> "AssistantManager" ;; "R2" -> "Manager" should just require a restart of the application, instead of having to make code changes in the controller / action.
I want to know if this is a valid approach or if there are security risks, even with this, and any better alternatives.
Ad 1. You read the setting using the configuration API, e.g. if this is the regular MVC you have ConfigurationManager.AppSettings to peek into app settings section of the web.config
Ad 2. You don't determine anything or rather, you seem to misunderstood the linked post. What you do is you put the Authorize over the controller (action) you want to secure and the OnAuthorization is fired when the controller / action is executed. If you really want, you can peek into the authorization context passed as the argument, the controller and action are available in the route data.
Ad 3. This is the easiest part, the currently logged user (or an anonymous user if the user is not yet authenticated) is passed in the authorizationContext.HttpContext.User property as the IPrincipal so you can even call its IsInRole method.
But what if we read (..and store in session??) all the authorization related configuration when the application first loads up
You really don't have to. Even if you read it from the configuration upon every request, the configuration is already preloaded upon every restart of the application, you don't really slow anything down much then with ConfigurationManager.AppSettings.
Any changes like "R1" -> "AssistantManager" ;; "R2" -> "Manager" should just require a restart of the application, instead of having to make code changes in the controller / action.
If you store it in the configuration file and modifying it triggers the restart of the app pool, you don't make any changes in the code.
I want to know if this is a valid approach or if there are security risks, even with this, and any better alternatives.
There are risks, someone who can access your app server can possibly reconfigure your app. Note however, that such someone could do any other harm as well, e.g. decompile, modify, recompile and reupload your app. Or even replace it with something completely else.
As for alternatives, it's completely impossible to come up with anything better if the criteria of what's better are vague. If something is possibly better we'd have to know what better stands for.
In other, simple words, this looks fine.

Handling Session_Start in an Orchard CMS module

A little background:
We need to develop a custom Orchard module for a client that will catch the external URL referrer (if any) and store it in a session variable for later use, e.g. submitting a query for one of their products.
My naive solution was to suggest we record the URL referrer on Session_Start because it is a reliable way of knowing how the user got to our site. The problem is that the client does not want us touching the global.asax.cs file. It has to be done via a custom module. This is non-negotiable.
So my question is this: how can I reliably retrieve and store the UrlReferrer information when a new session starts using an Orchard module?
Or alternatively, is there some other way I can hook into the page lifecycle and maybe check whether or not the previous page was an external referrer?
My most important concern here is I need to know whether someone clicked on a sponsored link and I need to find that out in a module, not global.asax.cs. I am not dead set on any particular tracking method, as long as it is possible in the Orchard framework given the limitations imposed on me.
FYI: Orchard version is 1.8+
You can do this from a filter. I implemented this a while ago in my commerce module, to enable giving discounts or attributions for conversions from partner sites, typically. You can see the source code for my filter here: https://github.com/bleroy/Nwazet.Commerce/blob/master/Filters/ReferrerFilter.cs
What I would do, is create a custom module and in there a custom controller:
public class ReferrerController : Controller {
public ActionResult Index(string referrer) {
if (Session["Referrer"] != null) {
// do nothing, already used as entry point in the current session
} else {
// handle referrer, probably also some timestamp or hash
Session["Referrer"] = referrer; // save in session
}
return RedirectToRoute("~/"); // redirect to home
}
}
Obviously, also create a route for this. Then from the external referrers, go to this route where the referrers will be handled. (http://example.com/referrer?referrer=somereferrer)

Session gets lost when enabling OutputCache

I have a (working) MVC-application that uses Session properties on multiple parts:
return httpContext.Session[SPContextKey] as SharePointAcsContext;
(ignore that this is sharepoint; This problem isn't be SP-specific)
This works fine until I try to enable Outputcaching:
[OutputCache (Duration =600)]
public ActionResult Select() {
DoSelect();
}
When the content is cached, httpContext.Session becomes NULL.
Is there a way to keep the Session data and also use caching?
I found the solution myself. It took a while until I came to the conclusion that - if the data is cached - there shouldn't be any individual code at all that is run. Cause that should be the main purpose of the cache: Don't run any code when the data is cashed.
That led me to the conclusion that the code causing the problem must be run before the cache. And so the "bad boy" was easy to find. Another attribute (in this case an AuthorizeAttribute) that is before the OutputCache-Attribute in the code is still run when caching applies but cannot access the Session:
[Route("{id}")]
[UserAuth(Roles =Directory.GroupUser)]
[JsonException]
[OutputCache(Duration = 600)]
public ActionResult Select()
{
DoSelect();
}
Putting the UserAuth-Attribute BELOW the OutputCache-Attribute solved the problem

asp.net MapPageRoute loading page twice

Not sure whats going on, but I have a rewrite with two parameters. For some reason the page is loading twice when it's called. I know that it's the rewrite because it works fine when it's just one parameter. Thanks for any help.
This is in my Global.asax
routeCollection.MapPageRoute("RouteForAlbum", "album/{autoID}/{albumName}", "~/SitePages/AlbumView.aspx");
This is on my page load
if (!Page.IsPostBack)
{
string id = Page.RouteData.Values["autoID"].ToString();
string albuname = Page.RouteData.Values["albumName"].ToString();
}
Wow, found the answer after more searching. If you have javascript reference with ../ this causes issues with URL rewritting.
asp.net Multiple Page_Load events for a user control when using URL Routing
UDPATE:
This can also happen when using CSS3PIE together with ASP.net Routing and the two don't play nicely together.
Any CSS3PIE css styles with a URL in the value can cause the target page to execute the code behind multilple times. For me specifically, it was these two lines:
behavior: url(PIE.htc);
-pie-background: url(bg-image.png) no-repeat, linear-gradient(#FFFFFF, #53A9FF);
Changing the above two lines to start with a leading slash "/" fixed it along with specifying the whole path to the files.
behavior: url(/scripts/PIE-1.0.0/PIE.htc);
-pie-background: url(/scripts/PIE-1.0.0/bg-image.png) no-repeat, linear-gradient(#FFFFFF, #53A9FF);

Categories