Optimizing ASP.NET MVC Applications with Output Caching

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, such as reducing server round trips, reducing database server round trips, reducing network traffic, etc.

Keep the following in mind.

  • Avoid caching content that is unique per user.
  • Avoid caching contents that are accessed rarely.
  • Use caching for content that is 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 the user invokes the controller method to see records, the application loops through the entire process and executes the database query. This can actually decrease the application performance. So, we can advantage of the "Output Caching" that avoids executing database queries each time the user invokes the controller method. Here the view page is retrieved from the cache instead of invoking the controller method and doing redundant work.

Cached Content Locations

In the above paragraph I said, in Output Caching the view page is retrieved from the cache, so where is the content cached/stored?

Please note, that there is no 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 the following locations available; as of now, we can use anyone.

  1. Any
  2. Client
  3. Downstream
  4. Server
  5. None
  6. ServerAndClient

With "Any", the output cache is stored on the server where the request was processed. The recommended store cache is always on the server very carefully. You will learn about some security-related tips in the following "Don't use Output Cache".

How does Output Cache Work?

It is very important to understand how the "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 the same request is being made from three different clients (here three different browsers) and we are getting the same cached version (look at the time).

Output Cache

Okay, now let's look at the code, how I developed the one above, and how to make any controller action or method cacheable. Here it is

[OutputCache(Duration = 10, VaryByParam = "name")]

Just add the preceding label before the controller method. The duration is in seconds, 10 seconds here. If you don't provide a "Duration" value then the default will be used, 60 seconds. I am using VaryByParam=" name" and "VeryByParam" is something that makes many differences that you should care about that will be discussed later. "name" is a parameter passed by the user with the request to do database records filtering.

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 the code above, I'm looking at the "name" parameter passed by the user and then, depending on the name, selecting matching records with a LINQ query and then checking if the model has a number of records greater than zero then sending the model to the view else simply send the view (no model).

VaryByParam can be one of the following types.

Step 1. VaryByParam = "none": Think of it like, we don't want to care about the form parameter or query string parameter passed by the user from the view page. If I use "none" then it will create the same cached version of the content for every user who visits the website, and the content will only change after a specified number of seconds (here 10 seconds).

Let's use [OutputCache(Duration = 10, VaryByParam = "none")] in the code above and look at the behavior.

VaryByParam

In the above GIF, you can notice on the second request to see a list of records that contains "a" nothing happens, because it is displaying the cached data.

Step 2. VaryByParam ="name": This property enables you to create different cached versions of the content when a form parameter or query string parameter varies. In other words, if I find records matching the "ce" string then a new cache will be created by replacing the older one, again if I find records matching the "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 the code above and look at behavior.

Search Customer

In the above GIF note that on each new request with a different query string parameter or form parameter, a new cache is being created; look at the time it is changing. Here the use of the cache is that if I request the same thing that I requested previously then the cached version will be rendered, here it is.

GIF note

In the above GIF note that nothing happens (look at the time) when I continuously request the same information, rendering the cached version.

Step 3. VaryByParam ="*": We can use * for all parameters or a semi-colon-separated list to cache various versions. This works very similar to the one above (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 scenarios work the same, so use whichever one that makes you happy.

Check Web Page is Cache-able or not

Fiddler is a great tool if you want to check whether a requested web page is cache-able or not, here is a GIF image of it.

Web Page

In the above GIF you can see the GET request is not cacheable whereas the POST request is cacheable and with a max-age: of 10 seconds.

Don't use Output Cache

Here you will learn about some quick security-related issues and their prevention.

Danger 1

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 vulnerabilities.

[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 the code above, to see how the usernames are appearing in both (IE and Chrome) browsers, the GIF is given below. Username is also being cached and stored on the server for other users.

 Username

In the above controller action method we don't have a "VaryByCustom" or "Location" attribute with "OutputCache" to safeguard it, so by default, it uses Location = OutputCacheLocation.And that is dangerous in this case. If you are using membership in the web application then you should pay special attention. A few ways are given below, the first is more secure and recommendable.

1st Way

You can also take advantage of the VaryByCustom property in [OutputCache] by overriding HttpApplication.GetVaryByCustomString and checking HttpContext.Current.User.IsAuthenticated.

This is what I will create in the 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 the OutputCache attribute.

[OutputCache(Duration = 10, VaryByParam = "none", VaryByCustom = "LoggedUserName")]
public ActionResult Profiles()
{
    //...
}

Now for every user logged in on the website OutputCache will create a separate version, and it works great. We can even use Duration, VaryByParam, VaryByCustom, and Location attributes together to make it more productive, useful, and secure.

We can also enable separate cache entries for each browser, VaryByCustom can be set to the 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 override HttpApplication.GetVaryByCustomString.

[OutputCache(Duration = 10, VaryByParam = "none", VaryByCustom = "browser")]
public ActionResult Profiles()
{
    ...
}

2nd Way

See, this is less reliable but works. You should use Location = OutputCacheLocation.Client. If you don't, the login username will also be cached and stored on the server for other users and that is confusing & quite dangerous.

Here is the complete controller action method code.

[OutputCache(Duration = 10, VaryByParam = "none", Location = OutputCacheLocation.Client)]
public ActionResult Profiles()
{
    ...
}

Note 1. POST requests are not cached on the client, in other words, this will not work because it is a POST request and the caching location is on the client.

[HttpPost]
[OutputCache(Duration = 10, VaryByParam = "name", Location = OutputCacheLocation.Client)]
public ActionResult SearchCustomer(string name = "")
{
    ...
}

Note 2. If you are trying to test client-side caching (like the one given above) and hitting F5 then you are losing the client cache. The way the 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 willbe served.

Danger 2

If you want a more secure application then 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 the code above, you are combining OutputCaching and Authorize with an action method that contains your credit card information. And you know how OutputCaching stores data out of the database that is not as secure as a database. So you are broadcasting your private information 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 method when your large application has already been deployed.

So, there is an alternative to configure the OutputCache profile in the web. config file. By configuring output caching in the web configuration file, you can control it in 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 the entire application.

In the following code, you can see I have used a new attribute CacheProfile that maps to Cache10Seconds that is in the web. config.

[OutputCache(CacheProfile = "Cache10Seconds", VaryByCustom = "LoggedUserName")]
public ActionResult Profiles()
{
    // Code here
}

And then the web. config

<system.web>
  <caching>
    <outputCacheSettings>
      <outputCacheProfiles>
        <add name="Cache10Seconds" duration="10" varyByParam="none"/>
      </outputCacheProfiles>
    </outputCacheSettings>
  </caching>
  ...
</system.web>

Please note, that I moved Duration, VaryByParam, and Location (we can use it also) in the web. config but not VaryByCustom and the reason is, that it is used for overriding the rules.

Now, assume for any reason I want to disable caching for an entire application that has already been deployed to production, then you can simply modify the cache profiles defined in the web configuration file.

We can even disable it in the following.

<system.web>
  <caching>
      <outputCache enableOutputCache="false" />
      <outputCacheSettings>
          <outputCacheProfiles>
              <add name="Cache10Seconds" duration="10" varyByParam="none"/>
          </outputCacheProfiles>
      </outputCacheSettings>
  </caching>
</system.web>

This approach is pretty good because rather than targeting any specific outputCacheProfile we can disable it all at once, awesome.


Similar Articles