One of the nicest things about ASP.NET Core is the availability of certain singleton models that greatly simplify some very common developer needs. Given that (depending on who you ask) one of the two hardest problems in computing is caching1, it’s extremely helpful that ASP.NET Core ships with several models for caching data, chief of which are IMemoryCache
and IDistributedCache
, added to an ASP.NET Core application via dependency injection and then available to both the framework and the application itself. Although these two expose almost identical APIs, they differ rather significantly in semantics.2
As the name suggests, IMemoryCache
is an in-memory cache with ephemeral storage: its contents are lost when the application is restarted. As for IDistributedCache
, it’s a little confusing because the two are not actually counterparts: the opposite of an IMemoryCache
would be a hypothetical IPersistentCache
, whereas the primary motivation behind the existence of IDistributedCache
is not to add persistence to IMemoryCache
but rather turn it into an ostensibly shared cache accessible to multiple instances of an application sitting behind a load balancer (hopefully coherently so), and usually not on the same machine.
As such, IDistributedCache
does not officially have a persistence model—however by virtue of its out-of-process implementation, it somewhat guarantees key-value caching resilient to application or worker process recycling. As a result, both application developers and Microsoft themselves have taken to using it for persistence-related reasons as well.3 The problem is that the default implementations for IDistributedCache
are focused more on the “distributed” part rather than the “persistent” part, and a) don’t necessarily guarantee persistence, and b) aren’t necessarily intended to be used in non-distributed scenarios.
For production purposes, setting up the backend service fulfilling IDistributedCache
(e.g. redis, NCache, or SQL Server) is generally easy enough for any competent sysadmin, but during development and testing developers may find themselves without any form of persistent caching, as the local IMemoryCache
is lost at application startup but there may be no viable IDistributedCache
offering readily available on the testing machine. Also keep in mind that unlike many alternatives, ASP.NET Core is a zero-dependency framework and doesn’t require background services, docker containers, etc. — not even a web server or IIS Express — and as a result many projects have no “configure a development environment” step apart from “open the solution file,” but to properly test components that rely on an IDistributedCache
implementation to provide persistence, a developer has hitherto needed to install and configure 3rd party dependencies.
NeoSmart.Caching.Sqlite
is an open source project available as a nuget package for .NET Standard 2.0 and in particular for ASP.NET Core 2.2 implementing IDistributedCache
intended for single-machine deployment purposes (i.e. it is not actually a distributed cache but rather a persistent one), including both prototyping/development and final production deployment. It leverages the in-process SQLite database to provide persistent-to-disk key-value caching, and (unlike the other IDistributedCache
offerings) requires no installation, no background services, external network servers, or admin privileges to configure and deploy during both testing or production.
// using NeoSmart.Caching.Sqlite;
public void ConfigureServices(IServiceCollection services)
{
...
// Note: this *must* come before services.AddMvc()!
services.AddSqliteCache(options => {
options.CachePath = @"C:\data\bazaar\cache.db";
});
services.AddMvc();
...
}
The closest option to a single-machine persistent cache available to developers/sysadmins for .NET Core has probably been Microsoft SQL Express (via Microsoft.Caching.SqlServer
with a LocalDB
target configured in the connection string, but that has some considerable drawbacks:
- It required the separate installation (and maintenance, upgrades, security patching, etc) of Microsoft SQL Express,
- It required administrator privileges to initially install,
- It is almost impossible to deploy
LocalDB
in production, as the cache database should be pre-created at the command line but is user-specific, and by default IIS runs application pools in a restricted account with its own profile (and hence, its ownLocalDB
instances). Microsoft has indicated that localdb is not really intended for production use with IIS-hosted ASP.NET Core applications.
The redis editions of IDistributedCache
(via Microsoft.Caching.Redis
and Microsoft.Caching.StackExchangeRedis
) are also extremely popular, but have their own list of drawbacks, including
- There is no redis distribution available for Windows,4
- Developers wanting to use redis anyway would need to deploy a docker container to run the redis instance in the background to which they intend to connect to, or would otherwise have to configure an actual redis instance on a (possibly virtual) machine on the local network,
- The network configuration would necessarily defer from developer to developer, or else developers would be using shared redis instances for testing during development, leading to a headache either way.
- Finally, data persistence with redis is not very straightforward and depending on the version you have running may not be an option or else might need to be configured. (For things like data protection keys, not having guaranteed persistence semantics is a big deal breaker!)
Third party offerings such as the NCache
offerings suffer from similar drawbacks to SQL Server offering above, except they don’t even have the benefit of the LocalDB
option whereby the constantly running background service may be avoided, while still needing to install, update, and maintain a system-wide background service and requiring admin privileges to boot.
NeoSmart.Caching.Sqlite
suffers from none of these issues, and (as shown above) couldn’t be easier to use. What’s more,
- It’s implemented as an on-disk SQLite database, automatically created at first run (i.e. much easier to use than
Microsoft.Caching.SqlServer
withLocalDB
, which necessitates manual initialization of the datastore), to which all keys and values are persisted. - It doesn’t interop with the GC except when a cached value is retrieved and read into a managed object, so it suffers from none of the drawbacks that the default
IMemoryCache
suffers from with long lifetimes and/or large objects leading to forced GC gen 2 collections. NeoSmart.Caching.Sqlite
and all its dependencies are automatically installed and upgraded along with the rest of your project’s dependencies via NuGet, Paket, or whatever else you’re already using: there’s no background service that needs to separately installed and kept patched and up-to-date, no admin privileges are need to deploy it.- As such, your entire project can be deployed via MS Web Deploy without needing to install any third-party dependencies on the web server, etc.
NeoSmart.Sqlite.Caching
is fully parallelized with non-blocking reads and writes, thanks to all the hard work the SQLite project has put into creating thread-safe multi-reader/writer coherence out-of-the-box. On the .NET side of things, command readers are pre-created and pooled (and sometimes even lock-free to retrieve, thanks to theConcurrentBag
-backed command pool).
To install via NuGet, add a package reference to NeoSmart.Caching.Sqlite
or execute the following in the Package Manager Console:
Install-Package NeoSmart.Caching.Sqlite
NeoSmart.Caching.Sqlite
requires only that a rw-accessible path be specified where the database will be stored (it’ll be created if it doesn’t exist) and everything else is configured and deployed for you. After calling services.AddSqliteCache(...)
in your ConfigureServices()
method, the global SqliteCache
instance becomes available via dependency injection as both the specific SqliteCache
type and the abstract IDistributedCache
type.
Note that services.AddSqliteCache(...)
must come before the call to services.AddMvc()
, as Microsoft has simplified a lot of code within the framework by instantiating an instance of IMemoryCache
as a psuedo-IDistributedCache
if no IDistributedCache
is available.5 This is done within the call to services.AddMvc(...)
and will result in the wrong IDistributedCache
being returned if services.AddSqliteCache(...)
wasn’t called first!
The NeoSmart ASP.NET Core SQLite cache fully implements the IDistributedCache
interface, including both synchronous and asynchronous insert/get/remove/refresh operations as well as sliding renewal on retrieve. Expired items are removed periodically in the background,6 and won’t accumulate indefinitely.
The SQLite caching library for ASP.NET Core is released to the general public under the terms of the MIT open source license in hopes that it is useful and of some benefit. You can find it on GitHub to star and save, where you can also file any issues or pull requests. Improvements to the library or accompanying documentation are welcome.
Although that *probably* refers more to cache coherence rather than simply key-value persistence, to be perfectly frank. ↩
It is extremely refreshing to see Microsoft adopting the Haskell/Rust approach of using types to express/convey intention and semantics rather than merely shape. ↩
e.g. when no other secure storage can be found for data protection keys, the framework will try to store them in the
IDistributedCache
if it is available, otherwise they are regenerated at startup and the ability to decrypt secrets encrypted prior to the application restart is lost. ↩Microsoft at one point had released their own fork but that is no longer maintained nor supported, and other community-initiated forks are available but are insanely out-of-date and unpatched. ↩
This lets code thereafter load
IDistributedCache
via dependency injection and results in the correct cache being used regardless of whether the backing store is memory for a single PC or an external distributed cache for shared instances. ↩The default interval can be changed or the background cleanup can be disabled altogether via the
SqliteCacheOptions
flavor ofIOptions
, configured in the call toservices.AddSqliteCache(...)
. ↩
Model: K43U
MB Ver.:K43U
K43U-VX054
Check Number:3920
WARRANTY: 24
ID:1A
2011-09