Output Caching in MVC
In
this article you will learn everything about ‘Output Caching in MVC’. I will
take you by showing some real examples to make your view crystal clear.
Introduction
The
main purpose of using Output Caching is to dramatically improve the performance
of an ASP.NET MVC Application. It enables us to cache the content returned by
any controller method so that the same content does not need to be generated
each time the same controller method is invoked. Output Caching has huge advantages
like it reduces server round trips, reduces database server round trips,
reduces network traffic etc.
Keep
following in mind:-
Avoid
caching contents that are unique per user.
Avoid
caching contents that are accessed rarely.
Use
caching for contents that are accessed frequently.
Let’s
take an example. My MVC application displays a list of database records on the
view page so by default each time user invokes the controller method to see
records, application loops through the entire process and executes the database
query. And this is actually decreases the application performance. So, we can
take the advantage of ‘Output Caching’ that avoids executing database query
each time user invokes the controller method. Here the view page retrieved from
the cache instead of invoking controller method and doing redundant work.
Cached Content Locations
In
above paragraph I said, in Output Caching view page retrieved from the cache,
so where content is cached/stored?
Please
note, there is no any guarantee that content will be cached for the amount of time
that we specify. When memory resources become low, the cache starts evicting
content automatically.
OutputCache
label has a ‘Location’ attribute and it is fully controllable. Its default
value is ‘Any’, however there are following locations available as of
now, we can use any one.
1.
Any
2. Client 3. Downstream 4. Server 5. None 6. ServerAndClient
With
‘Any’, the output cache stored on the server where the request was processed.
Always recommended store cache on server very carefully. You will learn about some
security related tips in ‘Don’t use Output Cache’ below.
How Output Cache Works
It
is very important to understand how ‘Output Cache’ works. Anyone who
invokes a controller method will get the same cached version of the view page.
This means that the amount of work that the web server must perform to serve
the view page is dramatically reduced.
For
example, I have recorded a gif here to show you how same request is being made
from three different clients (here three different browsers) and we are getting
same cached version (look at the time).
Okay,
now let’s talk about the code, how I developed above one, how to make any
controller action or method cacheable. Here it is:
[OutputCache(Duration = 10,
VaryByParam = "name")]
Just
add above label before controller method. Duration is in second, 10 seconds here. If you
don’t provide ‘Duration’ value, the default will be used which is 60 seconds. I
am using VaryByParam="name" and ‘VeryByParam’ is something which makes lots of
differences that you should care about, will discuss it later. ‘name’ is a
parameter passed by user with request to do database records filering.
Here
is the complete code:
[HttpPost]
[OutputCache(Duration = 10, VaryByParam = "name")]
public ActionResult SearchCustomer(string name = "")
{
NorthwindEntities db = new NorthwindEntities();
var model = from r in
db.Customers
where r.ContactName.Contains(name)
select r;
if (model.Count() > 0)
{
return View(model);
}
else
{
return View();
}
}
In
above code, I’m looking at the 'name' parameter passed by user and then depending
on name selecting matching records with linq query and then checking if model
has number of records greater than zero then send the model to view else
send simply view (no model).
VaryByParam can be of following types:
1. VaryByParam = "none":
Think it like, we don’t want to care about form parameter or query string
parameter passed by user from view page. If I
use ‘none’, it will create same cached version of the content for every user who visits website,
and content will only change after specified seconds (here 10 seconds).
Let’s
use [OutputCache(Duration = 10,
VaryByParam = "none")] in above code and look at behavior.
In
above gif you can notice on second request to see list of records which contains 'a' nothing happens, because it is displaying the cached data.
2. VaryByParam = "name": This
property enables you to create different cached versions of the content when a
form parameter or query string parameter varies. Means if I find records
matching ‘ce’ string then a new cache will be created by replacing the older
one, again if I find records matching ‘ab’ string then a new cache will be
created by replacing the last one (‘ce’ cached), no matter duration is elapsed
or not.
Let’s
use [OutputCache(Duration = 10,
VaryByParam = "name")] in above code and look at behavior.
In
above gif you can notice on each new request with different query string parameter or form parameter a
new cache is being created, look at the time it is changing. Here the use of
cache is if I request same thing which I requested last time, the cached version
will be rendered, here it is.
In
above gif you can notice nothing happens (look at time) when I continuously
requesting for same information, rendering the cached version.
3. VaryByParam = "*":
We can use * for all parameters or a semi-colon separated list to cache
different versions. This works very similar to above one (VaryByParam=”name”).
[OutputCache(Duration = 10, VaryByParam = "*")]
public ActionResult SearchCustomer(string name = "", string city = "")
{
NorthwindEntities db = new NorthwindEntities();
...
OR
[OutputCache(Duration = 10, VaryByParam = "name; city")]
public ActionResult SearchCustomer(string name = "", string city = "")
{
NorthwindEntities db = new NorthwindEntities();
...
Both
scenario works same, so use any one that makes you happy.
Check Web Page is Cache-able or not?
Fiddler
is a great tool if you want to check whether requested web page is cache-able or
not, here is a gif image of the same.
In
above gif you can see GET request is not cacheable whereas POST request is
cacheable and with max-age: 10 seconds.
Don’t use Output Cache
Here you will know about some quick security related issues and its prevention.
Danger1:
We
should always be careful while using ‘OutputCache’, I will show you an example
here. Let’s look at the following controller action method and try finding security holes.
[OutputCache(Duration = 10, VaryByParam = "none")]
public ActionResult Profiles()
{
if (User.Identity.IsAuthenticated)
{
MembershipUser u = Membership.GetUser(User.Identity.Name);
ViewBag.welcomeNote = "Welcome back " + User.Identity.Name + ". Your last login date was " + u.LastLoginDate;
}
else
{
ViewBag.welcomeNote = "Welcome Guest";
}
return View();
}
Now,
I’m running above code, see how usernames appearing in both (IE and Chrome)
browsers, gif given below. Username is also being cached and
stored on server for other users.
In
above controller action method we don’t have 'VaryByCustom' or 'Location' attribute with
‘OutputCache’ to safeguard it, so by default it uses Location = OutputCacheLocation.Any which is
dangerous in this case. If you are using membership in web application you should pay
special attentions. Few ways given below, way 1 is more secure and recommendable.
1st
Way
You
can also take the advantage of VaryByCustom property in [OutputCache] by
overriding HttpApplication.GetVaryByCustomString and check
HttpContext.Current.User.IsAuthenticated.
This
is what I will create in Global.asax.cs file:
public override string GetVaryByCustomString(HttpContext
context, string custom)
{
if (custom == "LoggedUserName")
{
if (context.Request.IsAuthenticated)
{
return context.User.Identity.Name;
}
return null;
}
return base.GetVaryByCustomString(context,
custom);
}
And
then use it in OutputCache attribute:
[OutputCache(Duration = 10, VaryByParam = "none", VaryByCustom = "LoggedUserName")]
public ActionResult Profiles()
{
//...
}
Now
for every user who logged in on website OutputCache will create separate
version, and it works great. Even we can use Duration, VaryByParam,
VaryByCustom and Location attribute together to make it more productive, useful
and secure.
We
can also enable separate cache entries for each browser, VaryByCustom can be
set to a value of "browser". This functionality is built into the
caching module, and will insert separate cached versions of the page for each
browser name and major version. You don’t need to overriding
HttpApplication.GetVaryByCustomString.
[OutputCache(Duration = 10, VaryByParam = "none", VaryByCustom = "browser")]
public ActionResult Profiles()
{
...
2nd
Way
Look this is less reliable but works. You
should use Location = OutputCacheLocation.Client. If you don’t, login username will
be also cached and stored on server for other users which is confusing &
quite dangerous.
Here
is complete controller action method code.
[OutputCache(Duration = 10, VaryByParam = "none", Location = OutputCacheLocation.Client)]
public ActionResult Profiles()
{
...
}
Note1: POST
requests are not cached on the client, means this will not work because it is
POST request and the caching location is on client.
[HttpPost]
[OutputCache(Duration = 10, VaryByParam = "name", Location = OutputCacheLocation.Client)]
public ActionResult SearchCustomer(string name = "")
{
...
}
Note2:
If you are trying to test client side caching (as one given above) and hitting F5
then you are evicting the client cache. The way client cache is supposed to
work is that you have links on the site pointing to the Client action from some
other views and when the user clicks on those links the cached version will get
served.
Danger2:
If
you want more secure application, you should only enable caching for a page
when the page does not require authorization. Normally, you require
authorization for a page when you display personalized data in the page. Since
you don’t want personalized data to be shared among multiple users, don’t cache
pages that require authorization.
[Authorize]
[OutputCache(Duration = 10, VaryByParam = "none")]
public ActionResult CreditCardDetails()
{
...
}
In
above code, you are combining OutputCaching and Authorize with an action method
that contains your credit card information’s. And you know OutputCaching how stores
data out of database which is not secure like database. So you are broadcasting
your private information’s to the entire world. Don’t do it.
Creating Cache Profile
It
is very difficult to change the rules (like Duration, VaryByParam, VaryByCustom,
Location) used with ‘OutputCache’ on each controller methods when your large application
has already been deployed.
So,
there is an alternative to configure OutputCache profile in the web.config
file. By configuring output caching in the web configuration file, you can control
it on one central location. You can create one cache profile and apply the
profile to several controllers or controller actions. Also, you can modify the
web configuration file without recompiling your application. Any changes to the
web configuration file will be detected automatically and applied to whole
application.
In
the code given below you can see I have used a new attribute CacheProfile that
maps to Cache10Seconds which is in web.config.
[OutputCache(CacheProfile = "Cache10Seconds", VaryByCustom = "LoggedUserName")]
public ActionResult Profiles()
{
...
And
then web.config:
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="Cache10Seconds" duration="10" varyByParam="none"/>
</outputCacheProfiles>
</outputCacheSettings>
</caching>
...
Please
note, I moved Duration, VaryByParam, Location (we can use it also) in
web.config but not VaryByCustom and the reason is, it is used for overriding
the rules.
Now,
assume for any reason I want to disable caching for complete application that
has already been deployed to production, then you can simply modify the cache
profiles defined in the web configuration file.
<add name="Cache10Seconds" duration="10" varyByParam="none" enabled="false"/>
Even
we can disable it like follows:
<system.web>
<caching>
<outputCache
enableOutputCache="false" />
<outputCacheSettings>
<outputCacheProfiles>
<add name="Cache10Seconds" duration="10" varyByParam="none"/>
</outputCacheProfiles>
</outputCacheSettings>
</caching>
This
approach is pretty good because rather targeting any specific outputCacheProfile
we can disable all at once, awesome.
This
article getting very long so let me finish it here, please comment.
Hope
this helps.
really nice post, thanks
ReplyDeleteNice post
ReplyDeletereally nice post
ReplyDelete