Implementing Cache Dependency in ASP.NET Core
This article takes a look at how we can work with cache dependency with in-memory caching in ASP.NET Core.
Join the DZone community and get the full member experience.
Join For FreeCaching is a feature that has been introduced in ASP.NET. The latest versions of ASP.NET, as well as ASP.NET Core, provides support for caching. In ASP.NET Core, in particular, you’ve supported three different types of caching. These are response caching, in-memory caching, and distributed caching.
This article takes a look at how we can work with cache dependency with in-memory caching in ASP.NET Core. This article discusses the features and benefits of dotConnect for SQL, why caching is important, what cache dependency is all about, and why it is important.
In this example, we’ll connect to SQL Server using dotConnect for SQL Server (earlier known as SQLDirect.NET) which is a high performance and enhanced data provider for SQL Server that is built on top of ADO.NET and SqlClient and can work on both connected and disconnected modes.
Prerequisites
To be able to work with the code examples demonstrated in this article, you should have the following installed in your system:
- Visual Studio 2019 Community Edition
- SQL Server 2019 Developer Edition
- dotConnect for SQL Server
You can download .NET Core from here: https://dotnet.microsoft.com/download/archives
You can download Visual Studio 2019 from here: https://visualstudio.microsoft.com/downloads/
You can download SQL Server 2019 Developer Edition from here: https://www.microsoft.com/en-us/sql-server/sql-server-downloads
Create the Database
Now that the ASP.NET Core Web API project has been created in Visual Studio 2019; the next step is to create the database. Note that for the sake of simplicity we’ll use a database with just two tables with a simple design in this example.
Launch the SQL Server Management Studio and create a new database called Demo. Next, use the following script to create two tables named Products and Categories inside the Demo database.
x
CREATE TABLE [dbo].[Department](
[Id] [int] NOT NULL,
[Name] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_Department] PRIMARY KEY CLUSTERED ([Id] ASC) WITH (
PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF
) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
We’ll use this database in the subsequent sections of this article to demonstrate how we can work with caching and cache dependency in ASP.NET Core.
Create a New ASP.NET Core Web API Project in Visual Studio 2019
Once you’ve installed the necessary software and/or tools needed to work with dotConnect for SqlServer, follow the steps given below to create a new ASP.NET Core Web API project.
- First off, open the Visual Studio 2019 IDE
- Next, click "Create a new project" once the IDE has loaded
- Click "Create a new project"
- Next, select "ASP.NET Core Web Application"
- Click the "Next" button
- Specify the project name and location - where it should be stored in your system
- Optionally, click the "Place solution and project in the same directory" checkbox.
- Next, click the "Create" button
- In the "Create a new ASP.NET Core Web Application" dialog window that is shown next, select "API" as the project template.
- Select ASP.NET Core 3.1 or later as the version.
- You should disable the "Configure for HTTPS" and "Enable Docker Support" options by disabling the respective checkboxes.
- Since we'll not be using authentication in this example, specify authentication as "No Authentication".
- Finally, click on the "Create" button to finish the process.
Create a new controller class in this project and name it as DepartmentController. We’ll use this class in the sections that follow.
Enable InMemory Caching in ASP.NET Core
You can take advantage of the two interfaces IMemoryCache and IDistributedCache for working with caching in ASP.NET. We'll use IMemory cache in this article.
To be able to use in-memory caching in ASP.NET Core, you must enable it as shown in the code snippet given below:
x
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMemoryCache();
}
As you can see in the preceding code snippet, we’ve enables in-memory caching in the ConfigureServices method of the Startup class.
Store and Retrieve Items Using IMemoryCache
You can take advantage of the Set<T>() method to store an object in the cache using the IMemoryCache interface as shown below:
xxxxxxxxxx
_cache.Set("ThisIsMyKey", DateTime.Now.ToString());
The Set method accepts two parameters - the first one is the key that is an identifier using which you can identify the objects stored in the cache and the second one is the object to be cached.
The Get(object key) method can be used to retrieve a cached object.
xxxxxxxxxx
_cache.Get("ThisIsMyKey");
The Get method returns an object so you'd need to cast the returned instance as appropriate.
You can also use the Get<T>(object key) method to retrieve an object from the cache as shown below:
xxxxxxxxxx
_cache.Get<string>("ThisIsMyKey");
You can also use the TryGetValue<T>() method if you're not sure if a specific key is available in the cache.
x
DateTime currentDateTime;
if (!cache.TryGetValue < string > ("ThisIsMyKey", out currentDateTime))
{
currentDateTime = DateTime.UtcNow;
}
The GetOrCreate() method verifies if the key passed as an argument is available, if not, it creates it.
x
return _cache.GetOrCreate < DateTime > ("ABCXYZ", cacheEntry =>
{
return DateTime.UtcNow;
});
Implementing Cache Dependency in ASP.NET Core
Note that there is no Cache object in ASP.NET Core. You should implement caching using the IMemoryCache interface pertaining to Microsoft.Extensions.Caching.Memory namespace.
x
public interface IMemoryCache: IDisposable
{
bool TryGetValue(object key, out object value);
ICacheEntry CreateEntry(object key);
void Remove(object key);
}
In this example we’ll take advantage of dotConnect for SQL Server – it’s a high performant framework which can be used for writing efficient code and build flexible data access applications.
Creating a New Connection
To create a new Sql Connection object at runtime, you should first add references to the Devart.Data.SqlServer.dll and Devart.Data.dll assemblies.
The following code snippet illustrates how you can create SqlConnection at runtime.
xxxxxxxxxx
using Devart.Data.SqlServer;
using Devart.Data;
SqlConnection connection = new SqlConnection();
connection.DataSource = "LAPTOP-DEMO\MSSQLSERVER";
connection.Database = "Test";
connection.UserId = "some user name";
connection.Password = "some password";
connection.MaxPoolSize = 150;
connection.ConnectionTimeout = 30;
Alternatively, you can specify all of the above properties in a single statement as shown below:
xxxxxxxxxx
SqlConnection connection = new SqlConnection();
connection.ConnectionString = "User Id=sa;Password=some password;DataSource=LAPTOP-DEMO\MSSQLSERVER";
Create the Repository
We’ll now create a class named DepartmentRepository that follows the repository design pattern. The repository design pattern is a popular pattern that is used to abstract the calls to the database. It does this by exposing the necessary methods that the application can use to perform CRUD operations against the underlying database. In this example we’ll be having just one method named GetData that would return a list of departments. The DepartmentRepository class is given below:
x
public class DepartmentRepository
{
public List < Department > GetData()
{
try
{
List < Department > departments = new List < Department > ();
string connectionString =
"Specify your database connection string here.";
Devart.Data.SqlServer.SqlConnection dbConnection =
new Devart.Data.SqlServer.SqlConnection(connectionString);
Devart.Data.SqlServer.SqlDataAdapter dataAdapter =
new Devart.Data.SqlServer.SqlDataAdapter
("SELECT * From Department", dbConnection);
DataSet dataSet = new DataSet();
dataAdapter.Fill(dataSet, "Department");
foreach(DataTable dataTable in dataSet.Tables)
{
foreach(DataRow dataRow in dataTable.Rows)
{
Department department = new Department();
department.Id =
int.Parse(dataRow["Id"].ToString());
department.Name = dataRow["Name"].ToString();
departments.Add(department);
}
}
return departments;
} catch
{
throw;
}
}
}
You can take advantage of dependency injection to be able to use an instance of the DepartmentRepository in your controller classes. We’ll skip this discussion here anyway.
Set Expiration Policies on Cached Data
When working with in-memory cache using the IMemoryCache interface, you can take advantage of MemoryCacheEntryOptions class to manage the expiration of cache data. You can either mention a fixed time (absolute expiration) after which the cached item would expire, or mention a certain time (sliding expiration) after which the cached item will expire. The following code snippet illustrates how you can store items in the cache with absolute expiration:
xxxxxxxxxx
DepartmentRepository repository = new DepartmentRepository();
var data = repository.GetData();
_cache.Set("ABCXYZ", data, TimeSpan.FromDays(1));
The following code snippet shows how you can do the same with sliding expiration:
x
DepartmentRepository repository = new DepartmentRepository();
var data = repository.GetData();
_cache.Set("ABCXYZ", data, new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromDays(1)
});
You can use both absolute and sliding expiration together. The following code snippet illustrates how this can be achieved:
x
DepartmentRepository repository = new DepartmentRepository();
var data = repository.GetData();
_cache.Set("ABCXYZ", data, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(5),
SlidingExpiration = TimeSpan.FromDays(1)
});
Cache Dependencies and Callbacks
The MemoryCacheEntryOptions class provides support for registering callbacks, which would be executed when an item is removed from the memory cache. The following code snippet shows how it can be used:
x
MemoryCacheEntryOptions cacheOption = new MemoryCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = (DateTime.Now.AddMinutes(1) - DateTime.Now),
};
cacheOption.RegisterPostEvictionCallback(
(key, value, reason, substate) =>
{
Console.Write("Cache expired!");
});
The DefaultController class should look like the code snippet given below – just delete all other methods in it to keep things simple.
x
[Route("api/[controller]")]
[ApiController]
public class DefaultController: ControllerBase
{
private IMemoryCache _cache;
public DefaultController(IMemoryCache cache)
{
_cache = cache;
}
//Other methods
}
The following action method calls the GetDepartments method to retrieve all records of the Department table and then calls the AddDataToCache method to add the data retrieved to the in-memory cache. Lastly, it returns all records of the Department table.
x
[HttpGet("GetData")]
public ActionResult < List < Department >> GetData()
{
var data = GetDepartments();
AddDataToCache(data);
return Ok(data);
}
The GetDepartments and AddDataToCache methods are given below:
private List < Department > GetDepartments()
{
DepartmentRepository repository = new DepartmentRepository();
return repository.GetDepartments();
}
private void AddDataToCache(List < Department > departments)
{
var memoryCacheEntryOptions = new MemoryCacheEntryOptions();
memoryCacheEntryOptions.RegisterPostEvictionCallback
(CacheExpired_Callback);
memoryCacheEntryOptions.AddExpirationToken(new
CancellationChangeToken(new
CancellationTokenSource(TimeSpan.FromSeconds(45)).Token));
_cache.Set("ABCXYZ", departments, memoryCacheEntryOptions);
}
The CacheExpired_Callback is triggered when data in the cache has expired.
x
private void CacheExpired_Callback(object key, object value, EvictionReason reason, object state)
{
var existingDataInCache =
_cache.Get < List < Department >> ("ABCXYZ");
if (existingDataInCache == null)
{
AddDataToCache(value as List < Department > );
}
}
The complete source code of the DepartmentController is given below:
x
[Route("api/[controller]")]
[ApiController]
public class DefaultController: ControllerBase
{
private IMemoryCache _cache;
public DefaultController(IMemoryCache cache)
{
_cache = cache;
}
[HttpGet("GetData")]
public List < Department > GetData()
{
var data = GetDepartments();
AddDataToCache(data);
return data;
}
private List < Department > GetDepartments()
{
DepartmentRepository repository = new DepartmentRepository();
return repository.GetDepartments();
}
private void AddDataToCache(List < Department > departments)
{
var memoryCacheEntryOptions = new MemoryCacheEntryOptions();
memoryCacheEntryOptions.RegisterPostEvictionCallback
(CacheExpired_Callback);
memoryCacheEntryOptions.AddExpirationToken(new
CancellationChangeToken(new
CancellationTokenSource(TimeSpan.FromSeconds(45)).Token));
_cache.Set("ABCXYZ", departments, memoryCacheEntryOptions);
}
private void CacheExpired_Callback(object key, object value,
EvictionReason reason, object state)
{
var existingDataInCache =
_cache.Get < List < Department >> ("ABCXYZ");
if (existingDataInCache == null)
{
AddDataToCache(value as List < Department > );
}
}
}
Summary
CacheDependency is a great feature in ASP.NET as well as ASP.NET Core. You can take advantage of cache dependency to ensure that the data in the cache is always in sync with that of the database. You can write a callback function that is triggered at fixed intervals of time to fetch data from the database as soon as data in the cache has expired. Alternatively, you can refresh data in the cache after a fixed interval of time irrespective of whether there is data in the cache or the data in the cache is null.
In addition, we used dotConnect for SQL Server, a flexible framework that offers high-performance native SQL Server connectivity, but you can choose whichever alternative works best for you.
Opinions expressed by DZone contributors are their own.
Comments