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.

Comments

Post a Comment

Popular posts from this blog

Migrating database from ASP.NET Identity to ASP.NET Core Identity

Customize User's Profile in ASP.NET Identity System