A persistent cache for ASP.NET Core

An SQLite-powered IDistributedCache for testing and production

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 own LocalDB 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 with LocalDB, 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 the ConcurrentBag-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.

   


If you would like to receive a notification the next time we release a nuget package for .NET or release resources for .NET Core and ASP.NET Core, you can subscribe below. Note that you'll only get notifications relevant to .NET programming and development by NeoSmart Technologies. If you want to receive email updates for all NeoSmart Technologies posts and releases, please sign up in the sidebar to the right instead.


  1. Although that *probably* refers more to cache coherence rather than simply key-value persistence, to be perfectly frank. 

  2. 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. 

  3. 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. 

  4. 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. 

  5. 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. 

  6. The default interval can be changed or the background cleanup can be disabled altogether via the SqliteCacheOptions flavor of IOptions, configured in the call to services.AddSqliteCache(...)

Leave a Reply

Your email address will not be published. Required fields are marked *