On this page

Skip to content

Application of MemoryCache in ASP.NET MVC

Using ActionFilter to Cache Action Content

OutputCacheAttribute is an ActionFilter provided by MVC used to mark Action Methods for caching. If OutputCache is not specifically configured, the server-side cache of OutputCacheAttribute is implemented using MemoryCache.

Properties

  • Duration: The duration of the cache (in seconds).

  • Location: The storage location of the cache. For setting values, please refer to "OutputCacheLocation". A brief summary is as follows:

    • None: Disables caching.
    • Client: Browser client.
    • Server: Web Server.
    • Downstream: Client and Proxy Server.
    • ServerAndClient: Client and Web Server.
    • Any: Web Server, Client, and Proxy Server.
  • NoStore: Sets whether caching is disallowed.

  • VaryByXXX: Differentiates cached content based on Headers, Form, and Query parameters. For example, when performing report queries, caching should be configured based on different query conditions.

  • CacheProfile: Sets the Name of the caching scheme defined in the Config. Usually, a project will have several fixed caching schemes. To avoid modifying all code using a scheme when its content changes, caching settings for each scheme are defined in the Config, and each Action Method uses CacheProfile to specify the caching scheme.

TIP

NoStore and Location.None look similar, but their actual effects are different. The specific behaviors are as follows:

  • NoStore: Sets the Cache-Control header to no-store, which does not affect Web Server caching.
  • Location.None: Sets the Cache-Control header to no-cache and does not store the Web Server cache.

For the behavior of Cache-Control, please refer to "Cache-Control". The content for no-store and no-cache is excerpted as follows: no-store: Do not let the browser cache. no-cache: Use the cache, but check with the server for new content before every request.

For the execution results of each Location setting, you can refer to this article by Darkthread: "In-depth observation of ASP.NET OutputCache behavior".

Concrete Use Case

For pages with pure data query functionality (which should not contain links to edit pages—I have encountered cases where the cache was 30 seconds, and a customer quickly modified data on the edit page and returned, only to be confused why the data hadn't changed), if some data queries are time-consuming, you can use OutputCacheAttribute to create cached data.

Regarding caching for query functions, you can use VaryByParam="*" to create different cache versions for different QueryString or POST parameters. However, if permissions are set for different users, this will cause users with different permissions to cache the same result. Therefore, additional handling is required. The following is an implementation example.

Web.config

Please set the duration in seconds according to your project's situation.

xml
<system.web>
  <caching>
    <outputCacheSettings>
      <outputCacheProfiles>
        <add name="Default" duration="30" varyByParam="*" varyByCustom="Cookie" noStore="true" />
      </outputCacheProfiles>
    </outputCacheSettings>
  </caching>
</system.web>

Global.asax.cs

Override GetVaryByCustomString() to generate different keys for different users. The following provides two methods using Cookie and Session. If using Session, you must change the value of varyByCustom in "Web.config" to Session.

csharp
public class MvcApplication : HttpApplication {
    public override string GetVaryByCustomString(HttpContext context, string custom) {
        const string OutputCacheKey = "OutputCacheId";

        if (custom.Equals("Cookie", StringComparison.OrdinalIgnoreCase)) {
            if (Request.Cookies[OutputCacheKey] == null) {
                string cacheId = Guid.NewGuid().ToString();
                Response.Cookies.Add(new HttpCookie(OutputCacheKey) {
                    Value = cacheId,
                    HttpOnly = true,
                    Expires = DateTime.Now.AddHours(1),
                    Secure = false // Please set according to whether SSL is used
                });

                return cacheId;
            }

            return Request.Cookies[OutputCacheKey].Value;
        }

        // Please replace UserId with the actual Session Key of the logged-in account
        if (custom.Equals("Session", StringComparison.OrdinalIgnoreCase)
            && Session["UserId"] != null
        ) {
            string userId = Session["UserId"].ToString();

            if (Session[OutputCacheKey] == null
                || !(Session[OutputCacheKey] is VaryByCustomInfo customInfo)
                || customInfo.UserId == userId
            ) {
                Guid value = Guid.NewGuid();
                Session[OutputCacheKey] = new VaryByCustomInfo(userId, value);
                return value.ToString();
            }

            return customInfo.Value.ToString();
        }

        return base.GetVaryByCustomString(context, custom);
    }

    private class VaryByCustomInfo {
        public VaryByCustomInfo(string userId, Guid value) {
            UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        }

        public string UserId { get; }

        public Guid Value { get; }
    }
}

Controller

You can set a breakpoint in Index to test. You will find that as long as the model content is the same and the same person is browsing the page, the breakpoint will only be triggered on the first visit within a certain period.

csharp
public class TestController : Controller {
    [OutputCache(CacheProfile = "Default")]
    [HttPost]
    public ActionResult Index(IndexViewModel model) {
        //...Implementation to generate ActionResult using model...
    }
}

Clearing Cache Data When Updating the Database

Some data that is small in volume and does not change frequently can be stored in the cache to reduce the number of database connections, such as city/county data and website settings from the database. To avoid having stale data in the cache when data is modified, you must clear or update the cache data in the API that modifies the data. However, if you modify the database directly, there is still a possibility of caching old data. Therefore, the best practice is to monitor the database data directly and clear the cache when the data changes.

ChangeMonitor

MemoryCache can use ChangeMonitor to detect whether the data source has changed. The .NET Framework provides the following two implementation classes:

  • HostFileChangeMonitor: Used to monitor file changes on the host.
  • SqlChangeMonitor: Used to detect data changes on SQL Server. SqlChangeMonitor uses "SqlDependency" to monitor database data changes. When SqlDependency is added to a SqlCommand, it creates a "SqlNotificationRequest" assigned to the SqlCommand to establish a notification request with SQL Server. When data is modified, SqlChangeMonitor notifies MemoryCache to clear the cached data.

Example

Global.asax.cs

csharp
public class MvcApplication : System.Web.HttpApplication {
    protected void Application_Start() {
        //...Other implementations in Application_Start...

       // Add this line
       SqlDependency.Start(WebConfigurationManager.ConnectionStrings["MyDB"].ConnectionString);
    }

    protected void Application_End() {
        // Add this line
        SqlDependency.Stop(WebConfigurationManager.ConnectionStrings["MyDB"].ConnectionString);
    }
}

HomeController

csharp
public class HomeController : Controller {
    private static DateTime lastChangedTime;

    private const string CacheKey = "CacheKey";

    public ActionResult TestDependency() {
        // If there is no Cache data, create it
        if (MemoryCache.Default[CacheKey] is null) {
            CreateCache();
        }
        ViewBag.Key1 = MemoryCache.Default[CacheKey] as string;
        ViewBag.LastChangedTime = lastChangedTime;

        return View();
    }

    private void CreateCache() {
        string connectionStr = WebConfigurationManager.ConnectionStrings["MyDB"].ConnectionString;

        CacheItemPolicy policy = new CacheItemPolicy();
        using (SqlConnection conn = new SqlConnection(connectionStr))
        using (SqlCommand cmd = new SqlCommand("SELECT Key1 FROM dbo.Config", conn)) {
            SqlDependency dependency = new SqlDependency(cmd);
            // OnChange can be used to update other data when data changes
            dependency.OnChange += SqlDependencyOnChange;

            conn.Open();
            // The SQL Command must be executed once for the monitoring to take effect; you can decide whether to retrieve data at the same time
            string key1 = cmd.ExecuteScalar().ToString();

            SqlChangeMonitor monitor = new SqlChangeMonitor(dependency);
            policy.ChangeMonitors.Add(monitor);

            // Set cache data; when data changes, clear the cache
            MemoryCache.Default.Set(CacheKey, key1, policy);
        }
    }

    private void SqlDependencyOnChange(object sender, SqlNotificationEventArgs e) {
        lastChangedTime = DateTime.Now;
        (sender as SqlDependency).OnChange -= SqlDependencyOnChange;
    }
}

WARNING

  • To enable data monitoring, the Service Broker feature must be enabled in the database.
  • The SQL syntax used to monitor data must specify the specific columns to be monitored, and the table name must include the Schema (e.g., dbo), otherwise, the cache data cannot be created correctly.
  • After setting SqlCommand for SqlDependency, the SqlCommand must be executed once for it to take effect.

Enabling Service Broker

If Service Broker is not yet enabled, you can use the following syntax to enable it:

sql
ALTER DATABASE {DatabaseName} SET ENABLE_BROKER;

If you detach and re-attach a database that already has Service Broker enabled and execute this syntax, you might encounter the following error message:

text
Service Broker cannot be enabled in database "<DBName>" because the Service Broker GUID in the database (<GUID>) does not match the one in sys.databases (<GUID>).

In this case, you need to use the following syntax to reset the Service Broker:

sql
ALTER DATABASE {DatabaseName} SET NEW_BROKER;

Since enabling Service Broker must be done while no other users are connected, if it is a running database, you need to add WITH ROLLBACK IMMEDIATE when executing the above syntax to roll back incomplete transactions and disconnect other users from the database. Therefore, the complete syntax is as follows:

sql
ALTER DATABASE {DatabaseName} SET NEW_BROKER WITH ROLLBACK IMMEDIATE;

Change History

  • 2022-11-14 Initial version of the document created.