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-Controlheader tono-store, which does not affect Web Server caching. - Location.None: Sets the
Cache-Controlheader tono-cacheand 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.
<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.
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.
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.
SqlChangeMonitoruses "SqlDependency" to monitor database data changes. WhenSqlDependencyis added to aSqlCommand, it creates a "SqlNotificationRequest" assigned to theSqlCommandto establish a notification request with SQL Server. When data is modified,SqlChangeMonitornotifiesMemoryCacheto clear the cached data.
Example
Global.asax.cs
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
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
SqlCommandforSqlDependency, theSqlCommandmust 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:
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:
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:
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:
ALTER DATABASE {DatabaseName} SET NEW_BROKER WITH ROLLBACK IMMEDIATE;Change History
- 2022-11-14 Initial version of the document created.
