Akavache: An Asynchronous Key-Value Store for Native Applications
Akavache is an asynchronous, persistent (i.e. writes to disk) key-value
store created for writing desktop and mobile applications in C#, based on
SQLite3. Akavache is great for both storing important data (i.e. user
settings) as well as cached local data that expires.
Where can I use it?
Akavache is currently compatible with:
- Xamarin.iOS / Xamarin.Mac / Xamarin.Android / Xamarin.TVOS / Xamarin.WatchOS
- Maui iOS / Mac / Mac Catalyst / Android / TVOS
- .NET 4.6.2 (and above) and .NET 6 Desktop (WPF and WinForms)
- .NET 6.0
- Windows 10 (Universal Windows Platform)
- Tizen 4.0
What does that mean?
Downloading and storing remote data from the internet while still keeping the
UI responsive is a task that nearly every modern application needs to do.
However, many applications that don't take the consideration of caching into
the design from the start often end up with inconsistent, duplicated code for
caching different types of objects.
Akavache is a library that makes common app
patterns easy, and unifies caching of different object types (i.e. HTTP
responses vs. JSON objects vs. images).
It's built on a core key-value byte array store (conceptually similar to a
Dictionary<string, byte[]>
), and on top of that store, extensions are
added to support:
- Arbitrary objects via JSON.NET
- Fetching and loading Images and URLs from the Internet
- Storing and automatically encrypting User Credentials
Contents
Getting Started
Interacting with Akavache is primarily done through an object called
BlobCache
. At App startup, you must first set your app's name via
BlobCache.ApplicationName
or Akavache.Registrations.Start("ApplicationName")
. After setting your app's name, you're ready to save some data.
For example with Xamarin Forms or WPF applications you'll place this in the constructor of your App.xaml.cs
file.
Choose a location
There are four built-in locations that have some magic applied on some systems:
BlobCache.LocalMachine
- Cached data. This data may get deleted without notification.
BlobCache.UserAccount
- User settings. Some systems backup this data to the cloud.
BlobCache.Secure
- For saving sensitive data - like credentials.
BlobCache.InMemory
- A database, kept in memory. The data is stored for the lifetime of the app.
The magic
- Xamarin.iOS may remove data, stored in
BlobCache.LocalMachine
, to free up disk space (only if your app is not running). The locations BlobCache.UserAccount
and BlobCache.Secure
will be backed up to iCloud and iTunes. Apple Documentation
- Xamarin.Android may also start deleting data, stored in
BlobCache.LocalMachine
, if the system runs out of disk space. It isn't clearly specified if your app could be running while the system is cleaning this up. Android Documentation
- Windows 10 (UWP) will replicate
BlobCache.UserAccount
and BlobCache.Secure
to the cloud and synchronize it to all user devices on which the app is installed UWP Documentation
Platform-specific notes
- Windows 10 (Universal Windows Platform) - You must mark your application as
x86
or ARM
, or else you will get a strange runtime error about SQLitePCL_Raw not
loading correctly. You must also ensure that the Microsoft Visual C++ runtime
is added to your project.
Handling Xamarin/Maui Linker
There are two options to ensure the Akavache.Sqlite3 dll will not be removed by Xamarin and Maui build tools
1) Add a file to reference the types
public static class LinkerPreserve
{
static LinkerPreserve()
{
var persistentName = typeof(SQLitePersistentBlobCache).FullName;
var encryptedName = typeof(SQLiteEncryptedBlobCache).FullName;
}
}
2) Use the following initializer in your cross platform library or in your head project
Akavache.Registrations.Start("ApplicationName")
Using Akavache
The most straightforward way to use Akavache is via the object extensions:
using System.Reactive.Linq;
Akavache.Registrations.Start("AkavacheExperiment")
var myToaster = new Toaster();
await BlobCache.UserAccount.InsertObject("toaster", myToaster);
var toaster = await BlobCache.UserAccount.GetObject<Toaster>("toaster");
Toaster toaster;
BlobCache.UserAccount.GetObject<Toaster>("toaster")
.Subscribe(x => toaster = x, ex => Console.WriteLine("No Key!"));
Handling Errors
When a key is not present in the cache, GetObject throws a
KeyNotFoundException (or more correctly, OnError's the IObservable). Often,
you would want to return a default value instead of failing:
Toaster toaster;
try {
toaster = await BlobCache.UserAccount.GetObject("toaster");
} catch (KeyNotFoundException ex) {
toaster = new Toaster();
}
toaster = await BlobCache.UserAccount.GetObject<Toaster>("toaster")
.Catch(Observable.Return(new Toaster()));
Shutting Down
Critical to the integrity of your Akavache cache is the BlobCache.Shutdown()
method. You must call this when your application shuts down. Moreover, be sure to wait for the result:
BlobCache.Shutdown().Wait();
Failure to do this may mean that queued items are not flushed to the cache.
Using a different SQLitePCL.raw bundle
To use a different SQLitePCL.raw bundle, e.g. Microsoft.AppCenter:
- Install the
akavache.sqlite3
nuget instead of akavache
- Install the SQLitePCLRaw bundle you want to use, e.g.,
SQLitePCLRaw.bundle_green
- Use
Akavache.Sqlite3.Registrations.Start("ApplicationName", () => SQLitePCL.Batteries_V2.Init());
in your platform projects or in your cross platform project.
<PackageReference Include="akavache.sqlite3" Version="6.0.40-g7e90c572c6" />
<PackageReference Include="SQLitePCLRaw.bundle_green" Version="1.1.11" />
Akavache.Sqlite3.Registrations.Start("ApplicationName", () => SQLitePCL.Batteries_V2.Init());
For more info about using your own versions of SqlitePCL.raw
Examining Akavache caches
Using Akavache Explorer, you
can dig into Akavache repos for debugging purposes to see what has been stored.

What's this Global Variable nonsense?
Why can't I use $FAVORITE_IOC_LIBRARY?
You totally can. Just instantiate SQLitePersistentBlobCache
or
SQLiteEncryptedBlobCache
instead - the static variables are there just to make it
easier to get started.
DateTime/DateTimeOffset Considerations
Our default implementation overrides BSON to read and write DateTime's as UTC.
To override the reader's behavior you can set BlobCache.ForcedDateTimeKind
as in the following example:
BlobCache.ForcedDateTimeKind = DateTimeKind.Local;
DateTime
are stored as ticks for high precision.
DateTimeOffset
are stored as ticks for both the Date/Time aspect and the offset.
Basic Method Documentation
Every blob cache supports the basic raw operations given below (some of them are
not implemented directly, but are added on via extension methods):
IObservable<byte[]> Get(string key);
IObservable<IDictionary<string, byte[]>> Get(IEnumerable<string> keys);
IObservable<T> GetObject<T>(string key);
IObservable<IEnumerable<T>> GetAllObjects<T>();
IObservable<IDictionary<string, T>> GetObjects<T>(IEnumerable<string> keys);
IObservable<Unit> Insert(string key, byte[] data, DateTimeOffset? absoluteExpiration = null);
IObservable<Unit> Insert(IDictionary<string, byte[]> keyValuePairs, DateTimeOffset? absoluteExpiration = null);
IObservable<Unit> InsertObject<T>(string key, T value, DateTimeOffset? absoluteExpiration = null);
IObservable<Unit> InsertObjects<T>(IDictionary<string, T> keyValuePairs, DateTimeOffset? absoluteExpiration = null);
IObservable<Unit> Invalidate(string key);
IObservable<Unit> Invalidate(IEnumerable<string> keys);
IObservable<Unit> InvalidateObject<T>(string key);
IObservable<Unit> InvalidateObjects<T>(IEnumerable<string> keys);
IObservable<Unit> InvalidateAll();
IObservable<Unit> InvalidateAllObjects<T>();
IObservable<IEnumerable<string>> GetAllKeys();
IObservable<DateTimeOffset?> GetCreatedAt(string key);
IObservable<DateTimeOffset?> GetObjectCreatedAt<T>(string key);
IObservable<IDictionary<string, DateTimeOffset?>> GetCreatedAt(IEnumerable<string> keys);
IObservable<Unit> Flush();
IObservable<Unit> Vacuum();
Extension Method Documentation
On top of every IBlobCache
object, there are extension methods that help with
common application scenarios:
IObservable<Unit> SaveLogin(string user, string password, string host = "default", DateTimeOffset? absoluteExpiration = null);
IObservable<LoginInfo> GetLoginAsync(string host = "default");
IObservable<Unit> EraseLogin(string host = "default");
IObservable<byte[]> DownloadUrl(string url,
IDictionary<string, string> headers = null,
bool fetchAlways = false,
DateTimeOffset? absoluteExpiration = null);
IObservable<IBitmap> LoadImage(string key, float? desiredWidth = null, float? desiredHeight = null);
IObservable<IBitmap> LoadImageFromUrl(string url,
bool fetchAlways = false,
float? desiredWidth = null,
float? desiredHeight = null,
DateTimeOffset? absoluteExpiration = null);
IObservable<T> GetOrFetchObject<T>(string key, Func<Task<T>> fetchFunc, DateTimeOffset? absoluteExpiration = null);
IObservable<T> GetOrCreateObject<T>(string key, Func<T> fetchFunc, DateTimeOffset? absoluteExpiration = null);
IObservable<T> GetAndFetchLatest<T>(this IBlobCache This,
string key,
Func<IObservable<T>> fetchFunc,
Func<DateTimeOffset, bool> fetchPredicate = null,
DateTimeOffset? absoluteExpiration = null,
bool shouldInvalidateOnError = false,
Func<T, bool> cacheValidationPredicate = null)