Persisted Cache is a simple caching library that allows you to turn any stateful resource into a key-value store. It is designed to be simple and easy to use, meanwhile it spares you the hassle and costs of managing a separate cache server.
Caching improves performance, but running and maintaining a dedicated cache infrastructure (like Redis) is often unnecessary for many applications. PersistedCache provides a simple alternative: a distributed cache backed by resources you already have.
Use PersistedCache when:
-
You don't want to run a dedicated cache server — Many systems introduce Redis purely for caching. PersistedCache allows you to reuse existing infrastructure like MySQL, PostgreSQL, SQLite, or the filesystem instead.
-
Your application runs on multiple instances — The cache can be shared across application instances, allowing horizontally scaled services to benefit from the same cached values.
-
You want cache data to survive restarts — Unlike in-memory caches, persisted entries remain available across application restarts or deployments.
-
Your team prefers operational simplicity — No additional infrastructure, monitoring, or scaling considerations. Just install a package and use the backing store you already operate.
-
You need caching close to your data — PersistedCache works well when your application already depends on a database and you want caching without introducing another system.
⚡ Faster responses through caching
🧩 No additional infrastructure like Redis
🔁 Shared cache across multiple instances
💾 Cache persistence across restarts
🛠 Pluggable storage backends (SQL, filesystem, etc.)
PersistedCache may not be the right solution if:
- You need extremely low latency caching (<1ms) — Database-backed caching involves network I/O and will be slower than in-memory solutions like Redis or Memcached.
- You already operate Redis or Memcached — If you have dedicated cache infrastructure already running, there's no reason to introduce another layer.
- You expect very high cache throughput — Heavy cache traffic benefits from specialized cache systems designed for performance at scale.
Install the specific package for the resource you want to use. For example, if you want to use MySQL as the cache store, you would install the PersistedCache.MySql package. (Installing the base package is not necessary)
dotnet add package PersistedCache.MySqlOr simply add it from the NuGet package gallery.
Currently supported resources are:
-
MySQL- PersistedCache.MySql -
PostgreSQL- PersistedCache.PostgreSql -
SQL Server- PersistedCache.SqlServer -
File System- PersistedCache.FileSystem -
SQLite- PersistedCache.Sqlite -
MongoDB- PersistedCache.MongoDb
The reason that the version does not match is due to semantic versioning. The base package is versioned independently of the resource packages. All packages are on the latest version, regardless of the version number.
services.AddMySqlPersistedCache("Your connection string here", options =>
{
// The name of the table to use for the cache
options.TableName = "persisted_cache";
// Can be set to false after the table is created (or if you want to manage the table yourself)
options.CreateTableIfNotExists = true;
// Purges expired entries based on configured expiry interval
options.PurgeExpiredEntries = true;
// The interval at which the cache purges expired entries
options.PurgeInterval = TimeSpan.FromHours(24);
// If you need to serialize/deserialize objects differently
options.JsonOptions = new JsonSerializerOptions()
});All the options shown above are optional and have default values, only the connection string is required.
public class MyService(IPersistedCache cache)
{
// Set a value in the cache
public void SetSomething()
{
cache.Set("my-key", "some value", Expire.InMinutes(5));
}
// Get a value from the cache
public string? GetSomething()
{
return cache.Get<string>("my-key");
}
// Get a value from the cache or set it if it doesn't exist
public void GetOrSetSomething()
{
var value = cache.GetOrSet("my-key", () => new RandomObject(), Expire.InMinutes(5));
}
// Query values from the cache using a pattern
public IEnumerable<RandomObject> QuerySomething()
{
return cache.Query<RandomObject>("my-*");
}
// Check if a value exists in the cache
public bool HasSomething()
{
return cache.Has("my-key");
}
// Forget a value from the cache
public void ForgetSomething()
{
cache.Forget("my-key");
}
// Flush values matching a pattern from the cache
public void FlushPattern()
{
cache.Flush("my-*");
}
// Flush all values from the cache
public void FlushAll()
{
cache.Flush();
}
// Get a value from the cache and remove it
public RandomObject? PullSomething()
{
return cache.Pull<RandomObject>("my-key");
}
// Purge the cache of expired entries
public void PurgeCache()
{
cache.Purge();
}
// Set a value in the cache asynchronously
public async Task SetSomethingAsync()
{
await cache.SetAsync("my-async-key", new RandomObject(), Expire.Never);
}
// Get a value from the cache asynchronously
public async Task<RandomObject?> GetSomethingAsync()
{
return await cache.GetAsync<RandomObject>("my-async-key");
}
// Get a value from the cache or set it if it doesn't exist asynchronously
public async Task<RandomObject?> GetOrSetSomethingAsync()
{
return await cache.GetOrSetAsync("my-async-key", async (ct) => await GetRandomObjectAsync(), Expire.InSeconds(5));
}
}Be aware when using value types, as the cache will return the default value if the key does not exist instead of null, unless you use a nullable value in the generic type.
If you need to use more than one Persisted Cache, you can use the driver based injection.
services.AddMySqlPersistedCache("Your connection string here");
services.AddFileSystemPersistedCache("Your path here");Then you can inject the cache you need in your service.
public class MyService(
IPersistedCache<MySqlDriver> mySqlCache,
IPersistedCache<FileSystemDriver> fileSystemCache
) {
public void SetSomething()
{
mySqlCache.Set("my-key", "some value", Expire.InMinutes(5));
fileSystemCache.Set("my-key", "some value", Expire.InMinutes(5));
}
}The first cache registered will be the default cache, so you can use the IPersistedCache interface without specifying the driver.
| Method | Description |
|---|---|
Set<T>(string key, T value, Expire expiry) |
Set a value in the cache with an expiry time |
SetAsync<T>(string key, T value, Expire expiry, CancellationToken cancellationToken = default) |
Set a value in the cache with an expiry time asynchronously |
Get<T>(string key) |
Get a value from the cache |
GetAsync<T>(string key, CancellationToken cancellationToken = default) |
Get a value from the cache asynchronously |
GetOrSet<T>(string key, Func<T> valueFactory, Expire expiry) |
Get a value from the cache or set it if it doesn't exist |
GetOrSet<T>(string key, Func<PersistedCacheEntryOptions, T> valueFactory) |
Get a value from the cache or set it if it doesn't exist with options |
GetOrSetAsync<T>(string key, Func<T> valueFactory, Expire expiry, CancellationToken cancellationToken = default) |
Get a value from the cache or set it if it doesn't exist asynchronously |
GetOrSetAsync<T>(string key, Func<Task<T>> valueFactory, Expire expiry, CancellationToken cancellationToken = default) |
Get a value from the cache or set it if it doesn't exist asynchronously |
GetOrSetAsync<T>(string key, Func<CancellationToken, Task<T>> valueFactory, Expire expiry, CancellationToken cancellationToken = default) |
Get a value from the cache or set it if it doesn't exist asynchronously |
GetOrSetAsync<T>(string key, Func<PersistedCacheEntryOptions, T> valueFactory, CancellationToken cancellationToken = default) |
Get a value from the cache or set it if it doesn't exist asynchronously with options |
GetOrSetAsync<T>(string key, Func<PersistedCacheEntryOptions, Task<T>> valueFactory, CancellationToken cancellationToken = default) |
Get a value from the cache or set it if it doesn't exist asynchronously with options |
GetOrSetAsync<T>(string key, Func<PersistedCacheEntryOptions, CancellationToken, Task<T>> valueFactory, CancellationToken cancellationToken = default) |
Get a value from the cache or set it if it doesn't exist asynchronously with options and cancellation token |
Query<T>(string pattern) |
Query values from the cache by pattern (* and ? wildcards supported) |
QueryAsync<T>(string pattern, CancellationToken cancellationToken = default) |
Query values from the cache by pattern asynchronously (* and ? wildcards supported) |
Has(string key) |
Check if a value exists in the cache |
HasAsync(string key, CancellationToken cancellationToken = default) |
Check if a value exists in the cache asynchronously |
Forget(string key) |
Forget a value from the cache |
ForgetAsync(string key, CancellationToken cancellationToken = default) |
Forget a value from the cache asynchronously |
Pull<T>(string key) |
Get a value from the cache and remove it |
PullAsync<T>(string key, CancellationToken cancellationToken = default) |
Get a value from the cache and remove it asynchronously |
Flush() |
Flush all values from the cache |
FlushAsync(CancellationToken cancellationToken = default) |
Flush all values from the cache asynchronously |
Flush(string pattern) |
Flush values from the cache by pattern |
FlushAsync(string pattern, CancellationToken cancellationToken = default) |
Flush values from the cache by pattern asynchronously |
Purge() |
Purge the cache of expired entries |
PurgeAsync(CancellationToken cancellationToken = default) |
Purge the cache of expired entries asynchronously |
If you want to contribute to this project, please feel free to open an issue or a pull request.
Want to make your own adapter? Add the PersistedCache package to your project and implement the IPersistedCache interface.