Preface
From November 11, 2017, when I first created the EasyCaching repository on GitHub, it has been nearly a year and a half. Most of the work was done after work and during holidays.
Since EasyCaching currently only has English documentation hosted on Read the Docs, and the MkDocs I chose initially does not yet support multiple languages, the Chinese documentation will be planned only after that support is added.
Previously, I saw some people in the group saying they couldn't find an introduction to EasyCaching, which is why I decided to write this blog post.
Let me briefly introduce EasyCaching below.
What is EasyCaching

EasyCaching, the name itself largely explains what it does — combining "easy" and "caching" together, its ultimate goal is to make caching operations more convenient for everyone.
Its development has gone through several important milestones:
- March 2018: Entered NCC with the help of Uncle Tea
- January 2019: Zhen Xi provided many improvement suggestions
- March 2019: NopCommerce introduced EasyCaching (see this commit record)
- April 2019: Listed in awesome-dotnet-core (I submitted the PR myself, a bit narcissistic...)
Before EasyCaching came out, most people were probably familiar with CacheManager, as they share similar positioning and functionality. Occasionally, I hear people comparing the two.
To help everyone make a better comparison, I will focus on introducing the existing features of EasyCaching below.
Main Features of EasyCaching
EasyCaching mainly provides the following features:
- Unified abstract caching interface
- Multiple common cache Providers (InMemory, Redis, Memcached, SQLite)
- Multiple serialization options for distributed cache data
- Hybrid cache (two-level caching)
- AOP operations for caching (able, put, evict)
- Multi-instance support
- Diagnostics support
- Redis-specific Provider
Besides these 8, there are some smaller features that I won't list here.
Let me introduce each of the above 8 features below.
Unified Abstract Caching Interface
Caching itself can be considered a data source, involving a set of CRUD operations. Therefore, a unified abstract interface is provided. By programming to interfaces, although EasyCaching provides some simple implementations that may not fully meet your needs, you are free to implement your own provider if you wish.
For cache operations, the following are currently provided, mostly with both synchronous and asynchronous versions:
- TrySet/TrySetAsync
- Set/SetAsync
- SetAll/SetAllAsync
- Get/GetAsync (with data retriever)
- Get/GetAsync (without data retriever)
- GetByPrefix/GetByPrefixAsync
- GetAll/GetAllAsync
- Remove/RemoveAsync
- RemoveByPrefix/RemoveByPrefixAsync
- RemoveAll/RemoveAllAsync
- Flush/FlushAsync
- GetCount
- GetExpiration/GetExpirationAsync
- Refresh/RefreshAsync (this will be deprecated later; just use set instead)
From the naming, you can guess what they do, so I won't elaborate further here.
Multiple Common Cache Providers
We divide these providers into two categories: local cache and distributed cache.
Currently, there are five implementations:
- Local cache: InMemory, SQLite
- Distributed cache: StackExchange.Redis, csredis, EnyimMemcachedCore
Their usage is very simple. Let's take the InMemory provider as an example.
First, install the corresponding NuGet package.
dotnet add package EasyCaching.InMemory
Then, add the configuration.
public void ConfigureServices(IServiceCollection services)
{
// Add EasyCaching
services.AddEasyCaching(option =>
{
// Simplest configuration using InMemory
option.UseInMemory("default");
//// Custom configuration using InMemory
//option.UseInMemory(options =>
//{
// // DBConfig is specific to each Provider
// options.DBConfig = new InMemoryCachingOptions
// {
// // Expiration scan frequency for InMemory, default is 60 seconds
// ExpirationScanFrequency = 60,
// // Maximum number of cache items in InMemory, default is 10000
// SizeLimit = 100
// };
// // Prevent all keys from expiring at the same time by adding a random number of seconds to each key's expiration, default is 120 seconds
// options.MaxRdSecond = 120;
// // Enable logging, default is false
// options.EnableLogging = false;
// // Lock lifetime, default is 5000 milliseconds
// options.LockMs = 5000;
// // Sleep time when the mutex lock is not acquired, default is 300 milliseconds
// options.SleepMs = 300;
// }, "m2");
//// Read configuration from file
//option.UseInMemory(Configuration, "m3");
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// If using Memcached or SQLite, you also need the following for initialization
app.UseEasyCaching();
}
Example configuration file:
"easycaching": {
"inmemory": {
"MaxRdSecond": 120,
"EnableLogging": false,
"LockMs": 5000,
"SleepMs": 300,
"DBConfig":{
"SizeLimit": 10000,
"ExpirationScanFrequency": 60
}
}
}
Regarding configuration, it is necessary to explain the
MaxRdSecondvalue because it once caused trouble for Brother Laomaozi. Its purpose is to prevent a large number of cache keys from expiring simultaneously. It adds a random number of seconds on top of each key's original expiration time to spread out the expiration times as much as possible. If your application scenario does not need this, you can set it to 0.
Finally, usage.
[Route("api/[controller]")]
public class ValuesController : Controller
{
// For a single provider, you can directly use IEasyCachingProvider
private readonly IEasyCachingProvider _provider;
public ValuesController(IEasyCachingProvider provider)
{
this._provider = provider;
}
// GET api/values/sync
[HttpGet]
[Route("sync")]
public string Get()
{
var res1 = _provider.Get("demo", () => "456", TimeSpan.FromMinutes(1));
var res2 = _provider.Get<string>("demo");
_provider.Set("demo", "123", TimeSpan.FromMinutes(1));
_provider.Remove("demo");
// others..
return "sync";
}
// GET api/values/async
[HttpGet]
[Route("async")]
public async Task<string> GetAsync(string str)
{
var res1 = await _provider.GetAsync("demo", async () => await Task.FromResult("456"), TimeSpan.FromMinutes(1));
var res2 = await _provider.GetAsync<string>("demo");
await _provider.SetAsync("demo", "123", TimeSpan.FromMinutes(1));
await _provider.RemoveAsync("demo");
// others..
return "async";
}
}
Another thing to note is that if you use a Get method with a data retriever, it will perform a locking operation before querying the database when the cache is missed, preventing the same key from querying the database multiple times at the same moment. The lock's lifetime and sleep time are determined by the LockMs and SleepMs configuration values.
Serialization Options for Distributed Cache
When operating with distributed caches, serialization is inevitable.
Currently, this mainly targets Redis and Memcached. Of course, there is a default serialization implementation based on BinaryFormatter, which does not depend on third-party libraries. If no other serializer is specified, this will be used.
In addition to the default implementation, three additional options are provided: Newtonsoft.Json, MessagePack, and Protobuf. Below is an example of using MessagePack with the Redis provider.
services.AddEasyCaching(option=>
{
// Use Redis
option.UseRedis(config =>
{
config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
}, "redis1")
// Use MessagePack to replace BinaryFormatter
.WithMessagePack()
//// Use Newtonsoft.Json to replace BinaryFormatter
//.WithJson()
//// Use Protobuf to replace BinaryFormatter
//.WithProtobuf()
;
});
However, note that currently, these serializers are not tied to a specific provider. This means you cannot have one provider using MessagePack and another using JSON. Only one serializer can be used at a time. This may need improvement in the future.
Multi-Instance Support
Some may ask what multi-instance means. Here, it refers to using multiple providers simultaneously in the same project, including multiple providers of the same type or different types.
This might be unclear, so let me give a fictitious example to make it clearer.
Suppose our product cache is in Redis cluster 1, user information is in Redis cluster 2, product reviews are in a Memcached cluster, and some simple configuration information is in the local cache of the application server.
In this scenario, it's obviously impossible for us to directly operate all these different caches simply through IEasyCachingProvider.
To handle multiple different caches simultaneously, we use IEasyCachingProviderFactory to specify which provider to use.
This factory obtains the provider by its name.
Let’s look at an example.
First, add two InMemory caches with different names:
services.AddEasyCaching(option =>
{
// Specify the current provider name as m1
option.UseInMemory("m1");
// Specify the current provider name as m2
config.UseInMemory(options =>
{
options.DBConfig = new InMemoryCachingOptions
{
SizeLimit = 100
};
}, "m2");
});
When using:
[Route("api/[controller]")]
public class ValuesController : Controller
{
private readonly IEasyCachingProviderFactory _factory;
public ValuesController(IEasyCachingProviderFactory factory)
{
this._factory = factory;
}
// GET api/values
[HttpGet]
[Route("")]
public string Get()
{
// Get the provider named m1
var provider_1 = _factory.GetCachingProvider("m1");
// Get the provider named m2
var provider_2 = _factory.GetCachingProvider("m2");
// provider_1.xxx
// provider_2.xxx
return $"multi instances";
}
}
In this example, provider_1 and provider_2 do not interfere with each other because they are different providers!
Intuitively, this is somewhat like the concept of regions, but strictly speaking, it is not a region.
AOP Operations for Caching
When talking about AOP, the first thing that comes to mind might be logging — logging parameters and results.
In caching operations, AOP also simplifies things.
Typically, we might operate on the cache like this:
public async Task<Product> GetProductAsync(int id)
{
string cacheKey = $"product:{id}";
var val = await _cache.GetAsync<Product>(cacheKey);
if(val.HasValue)
return val.Value;
var product = await _db.GetProductAsync(id);
if(product != null)
_cache.Set<Product>(cacheKey, product, expiration);
return val;
}
If caching is used extensively, this can become tedious.
We can use AOP to simplify this.
public interface IProductService
{
[EasyCachingAble(Expiration = 10)]
Task<Product> GetProductAsync(int id);
}
public class ProductService : IProductService
{
public Task<Product> GetProductAsync(int id)
{
return Task.FromResult(new Product { ... });
}
}
As you can see, we only need to add an attribute to the interface definition.
Of course, without proper configuration, just adding the attribute won't make it work. Below is an example using EasyCaching.Interceptor.AspectCore:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddScoped<IProductService, ProductService>();
services.AddEasyCaching(options =>
{
options.UseInMemory("m1");
});
return services.ConfigureAspectCoreInterceptor(options =>
{
// You can specify which provider to use here
// Or specify it on the attribute
options.CacheProviderName = "m1";
});
}
With these two steps, when you call a method, it will first try the cache. If no cache exists, it will execute the method.
Now let's discuss the parameters of the three attributes.
First, three common configurations:
| Configuration Name | Description |
|---|---|
| CacheKeyPrefix | Specifies the prefix for generating cache keys, typically used for update/delete operations |
| CacheProviderName | Specifies a custom provider name |
| IsHightAvailability | Whether business methods should continue to execute when cache-related operations throw exceptions |
EasyCachingAble and EasyCachingPut also have the following common configuration:
| Configuration Name | Description |
|---|---|
| Expiration | Key expiration time in seconds |
EasyCachingEvict has two special configurations:
| Configuration Name | Description |
|---|---|
| IsAll | Should be used with CacheKeyPrefix; deletes all keys with that prefix |
| IsBefore | Whether to delete the cache before or after executing the business method |
Diagnostics Support
To facilitate integration with third-party APM tools, Diagnostics support is provided for tracing.
Below is an example from our company's Jaeger integration:

Hybrid Cache (Two-Level Caching)
Two-level or multi-level caching is quite important in the caching world.
One of the most headache-inducing issues is how to achieve near real-time synchronization between different cache levels.
In EasyCaching, the hybrid cache implementation logic is roughly represented by the following diagram:

If the local cache on one server is modified, the cache bus will notify other servers to remove the corresponding local cache.
Here is a simple usage example.
First, install the NuGet packages:
dotnet add package EasyCaching.InMemory
dotnet add package EasyCaching.Redis
dotnet add package EasyCaching.HybridCache
dotnet add package EasyCaching.Bus.Redis
Then, add the configuration:
services.AddEasyCaching(option =>
{
// Add two basic providers
option.UseInMemory("m1");
option.UseRedis(config =>
{
config.DBConfig.Endpoints.Add(new Core.Configurations.ServerEndPoint("127.0.0.1", 6379));
config.DBConfig.Database = 5;
}, "myredis");
// Use hybrid cache
option.UseHybrid(config =>
{
config.EnableLogging = false;
// Cache bus subscription topic
config.TopicName = "test_topic";
// Local cache provider name
config.LocalCacheProviderName = "m1";
// Distributed cache provider name
config.DistributedCacheProviderName = "myredis";
});
// Use Redis as the cache bus
option.WithRedisBus(config =>
{
config.Endpoints.Add(new Core.Configurations.ServerEndPoint("127.0.0.1", 6379));
config.Database = 6;
});
});
Finally, usage:
[Route("api/[controller]")]
public class ValuesController : Controller
{
private readonly IHybridCachingProvider _provider;
public ValuesController(IHybridCachingProvider provider)
{
this._provider = provider;
}
// GET api/values
[HttpGet]
[Route("")]
public string Get()
{
_provider.Set(cacheKey, "val", TimeSpan.FromSeconds(30));
return $"hybrid";
}
}
If you find it unclear, you can check this complete example: EasyCachingHybridDemo.
Redis-Specific Provider
As everyone knows, Redis supports various data structures and atomic increment/decrement operations. To support these operations, EasyCaching provides a separate interface, IRedisCachingProvider.
This interface currently supports about 60-70% of common Redis operations; less frequently used ones are not included.
Similarly, this interface also supports multiple instances, and you can obtain different provider instances via IEasyCachingProviderFactory.
During injection, no additional steps are needed beyond adding Redis. The difference is that when using it, you use IRedisCachingProvider instead of IEasyCachingProvider.
Here is a simple usage example:
[Route("api/mredis")]
public class MultiRedisController : Controller
{
private readonly IRedisCachingProvider _redis1;
private readonly IRedisCachingProvider _redis2;
public MultiRedisController(IEasyCachingProviderFactory factory)
{
this._redis1 = factory.GetRedisProvider("redis1");
this._redis2 = factory.GetRedisProvider("redis2");
}
// GET api/mredis
[HttpGet]
public string Get()
{
_redis1.StringSet("keyredis1", "val");
var res1 = _redis1.StringGet("keyredis1");
var res2 = _redis2.StringGet("keyredis1");
return $"redis1 cached value: {res1}, redis2 cached value : {res2}";
}
}
In addition to these basic features, there are also some extensibility features. I would like to thank yrinleung for integrating EasyCaching with projects like WebApiClient and CAP. If interested, check out this project: EasyCaching.Extensions.
Final Thoughts
These are the features currently supported by EasyCaching. If you encounter any issues while using it, I hope you can provide active feedback to help EasyCaching become even better.
If you are interested in this project, feel free to give it a Star on GitHub, or join us in development and maintenance.
Recently, I opened an Issue to record users and cases currently using EasyCaching. If you are using EasyCaching and don't mind sharing your information, feel free to reply to that issue.


If you found this article helpful or insightful, please click the [Recommend] button at the bottom right, as your support is my greatest motivation to continue writing and sharing!
Author: Catcher Wong (Huang Wenqing)
Source: http://catcher1994.cnblogs.com/
Disclaimer: This article is copyrighted by the author and Blog Park. You may reprint it, but you must keep this statement and provide the original link in a prominent location on the article page; otherwise, the right to pursue legal action is reserved. If you find errors in the blog or have better suggestions or ideas, please contact me in time! To chat privately, you can send a private message or add my WeChat.