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. This project is tested with BrowserStack.
Akavache V11.1 introduces a new Builder Pattern for initialization, improved serialization support, and enhanced cross-serializer compatibility:
- ๐๏ธ Builder Pattern: New fluent API for configuring cache instances
- ๐ Multiple Serializer Support: Choose between System.Text.Json, Newtonsoft.Json, each with a BSON variant
- ๐ Cross-Serializer Compatibility: Read data written by different serializers
- ๐งฉ Modular Design: Install only the packages you need
- ๐ฑ Enhanced .NET MAUI Support: First-class support for .NET 9 cross-platform development
- ๐ Improved Security: Better encrypted cache implementation
Akavache V11.1 represents a significant evolution in the library's architecture, developed through extensive testing and community feedback in our incubator project. The new features and improvements in V11.1 were first prototyped and battle-tested in the ReactiveMarbles.CacheDatabase repository, which served as an experimental ground for exploring new caching concepts and architectural patterns.
Key Development Milestones:
- ๐งช Incubation Phase: The builder pattern, modular serialization system, and enhanced API were first developed and tested in ReactiveMarbles.CacheDatabase
- ๐ฌ Community Testing: Early adopters and contributors provided valuable feedback on the new architecture through real-world usage scenarios
- ๐ Production Validation: The incubator project allowed us to validate performance improvements, API ergonomics, and cross-platform compatibility before integrating into Akavache
- ๐ Iterative Refinement: Multiple iterations based on community feedback helped shape the final V11.1 API design and feature set
This careful incubation process ensured that V11.1 delivers not just new features, but a more robust, flexible, and maintainable caching solution that builds upon years of community experience and testing. The ReactiveMarbles organization continues to serve as a proving ground for innovative reactive programming concepts that eventually make their way into the broader ReactiveUI ecosystem.
- Quick Start
- Installation
- Migration from V10.x
- Configuration
- Serializers
- Cache Types
- Basic Operations
- Extension Methods
- Advanced Features
- Platform-Specific Notes
- Performance
- Best Practices
<PackageReference Include="Akavache.Sqlite3" Version="11.1.*" />
<PackageReference Include="Akavache.SystemTextJson" Version="11.1.*" />
Note:
WithAkavache
WithAkavacheCacheDatabase
andInitialize
always requires anISerializer
defined as a generic type, such asWithAkavache<SystemJsonSerializer>
. This ensures the cache instance is properly configured for serialization.
using Akavache.Core;
using Akavache.SystemTextJson;
using Akavache.Sqlite3;
using Splat.Builder;
// Initialize with the builder pattern
AppBuilder.CreateSplatBuilder()
.WithAkavacheCacheDatabase<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider() // REQUIRED: Explicitly initialize SQLite provider
.WithSqliteDefaults());
Important: Always call
WithSqliteProvider()
explicitly beforeWithSqliteDefaults()
. WhileWithSqliteDefaults()
will automatically callWithSqliteProvider()
if not already initialized (for backward compatibility), this automatic behavior is deprecated and may be removed in future versions. Explicit provider initialization is the recommended pattern for forward compatibility with other DI containers.
using Akavache.Core;
using Akavache.SystemTextJson;
using Akavache.Sqlite3;
using Splat.Builder;
// Example: Register Akavache with Splat DI
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(
"MyApp",
builder => builder.WithSqliteProvider() // REQUIRED: Explicit provider initialization
.WithSqliteDefaults(),
(splat, instance) => splat.RegisterLazySingleton(() => instance));
// For in-memory cache (testing or lightweight scenarios):
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(
"Akavache",
builder => builder.WithInMemoryDefaults(), // No provider needed for in-memory
(splat, instance) => splat.RegisterLazySingleton(() => instance));
using Akavache.Core;
using Akavache.SystemTextJson;
using Akavache.Sqlite3;
var akavacheInstance = CacheDatabase.CreateBuilder()
.WithSerializer<SystemJsonSerializer>()
.WithApplicationName("MyApp")
.WithSqliteProvider() // REQUIRED: Explicit provider initialization
.WithSqliteDefaults()
.Build();
// Use akavacheInstance.UserAccount, akavacheInstance.LocalMachine, etc.
// Store an object
var user = new User { Name = "John", Email = "john@example.com" };
await CacheDatabase.UserAccount.InsertObject("current_user", user);
// Retrieve an object
var cachedUser = await CacheDatabase.UserAccount.GetObject<User>("current_user");
// Store with expiration
await CacheDatabase.LocalMachine.InsertObject("temp_data", someData, DateTimeOffset.Now.AddHours(1));
// Get or fetch pattern
var data = await CacheDatabase.LocalMachine.GetOrFetchObject("api_data",
async () => await httpClient.GetFromJsonAsync<ApiResponse>("https://api.example.com/data"));
Akavache V11.1 uses a modular package structure. Choose the packages that match your needs:
<PackageReference Include="Akavache" Version="11.1.**" />
<!-- SQLite persistence -->
<PackageReference Include="Akavache.Sqlite3" Version="11.1.**" />
<!-- Encrypted SQLite persistence -->
<PackageReference Include="Akavache.EncryptedSqlite3" Version="11.1.**" />
<!-- System.Text.Json (fastest, .NET native) -->
<PackageReference Include="Akavache.SystemTextJson" Version="11.1.**" />
<!-- Newtonsoft.Json (most compatible) -->
<PackageReference Include="Akavache.NewtonsoftJson" Version="11.1.**" />
<!-- Image/Bitmap support -->
<PackageReference Include="Akavache.Drawing" Version="11.1.**" />
<!-- Settings helpers -->
<PackageReference Include="Akavache.Settings" Version="11.1.**" />
-
Initialization Method: The
BlobCache.ApplicationName
andRegistrations.Start()
methods are replaced with the builder pattern - Package Structure: Akavache is now split into multiple packages
- Serializer Registration: Must explicitly register a serializer before use
// V10.x initialization
BlobCache.ApplicationName = "MyApp";
// or
Akavache.Registrations.Start("MyApp");
// Usage
var data = await BlobCache.UserAccount.GetObject<MyData>("key");
await BlobCache.LocalMachine.InsertObject("key", myData);
// V11.1 initialization (RECOMMENDED: Explicit provider pattern)
AppBuilder.CreateSplatBuilder()
.WithAkavacheCacheDatabase<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider() // REQUIRED: Explicit provider initialization
.WithSqliteDefaults());
// Usage (same API)
var data = await CacheDatabase.UserAccount.GetObject<MyData>("key");
await CacheDatabase.LocalMachine.InsertObject("key", myData);
Create this helper method to ease migration:
public static class AkavacheMigration
{
public static void InitializeV11(string appName)
{
// Initialize with SQLite (most common V10.x setup)
// RECOMMENDED: Use explicit provider initialization
CacheDatabase
.Initialize<SystemJsonSerializer>(builder =>
builder
.WithSqliteProvider() // Explicit provider initialization
.WithSqliteDefaults(),
appName);
}
}
// Then in your app:
AkavacheMigration.InitializeV11("MyApp");
Akavache V11.1 uses a fluent builder pattern for configuration:
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp") // Required
.WithSqliteProvider() // Initialize SQLite backend
.WithSqliteDefaults()); // SQLite persistence
Explicit Provider Initialization (Recommended):
// โ
RECOMMENDED: Explicit provider initialization
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider() // Explicit provider initialization
.WithSqliteDefaults()); // Configure defaults
// โ
For encrypted SQLite
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithEncryptedSqliteProvider() // Explicit encrypted provider
.WithSqliteDefaults("password"));
Automatic Provider Initialization (Backward Compatibility Only):
// โ ๏ธ DEPRECATED: Automatic fallback behavior
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteDefaults()); // Automatically calls WithSqliteProvider() internally
Important: The automatic provider initialization in
WithSqliteDefaults()
is provided for backward compatibility only and may be removed in future versions for forward compatibility with other DI containers. Always use explicit provider initialization in new code.
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("TestApp")
.WithInMemoryDefaults());
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider() // REQUIRED: Must be called before WithSqliteDefaults()
.WithSqliteDefaults());
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithEncryptedSqliteProvider() // REQUIRED: Must be called before WithSqliteDefaults()
.WithSqliteDefaults("mySecretPassword"));
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithEncryptedSqliteProvider() // Provider must be initialized for custom SQLite caches
.WithUserAccount(new SqliteBlobCache("custom-user.db"))
.WithLocalMachine(new SqliteBlobCache("custom-local.db"))
.WithSecure(new EncryptedSqliteBlobCache("secure.db", "password"))
.WithInMemory(new InMemoryBlobCache()));
// Set global DateTime behavior
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider() // REQUIRED: Provider initialization
.WithForcedDateTimeKind(DateTimeKind.Utc)
.WithSqliteDefaults());
Akavache V11.1 supports multiple serialization formats with automatic cross-compatibility.
Best for: New applications, performance-critical scenarios, .NET native support
Features:
- โ Fastest performance
- โ Native .NET support
- โ Smallest memory footprint
- โ BSON compatibility mode available
- โ Limited customization options
Configuration:
var serializer = new SystemJsonSerializer()
{
UseBsonFormat = false, // true for max compatibility with old data
Options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
}
};
Best for: Migrating from older Akavache versions, complex serialization needs
Features:
- โ Maximum compatibility with existing data
- โ Rich customization options
- โ BSON compatibility mode
- โ Complex type support
- โ Larger memory footprint
- โ Slower than System.Text.Json
Configuration:
var serializer = new NewtonsoftSerializer()
{
UseBsonFormat = true, // Recommended for Akavache compatibility
Options = new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
NullValueHandling = NullValueHandling.Ignore
}
};
Once configured, pass the serializer type to the builder:
AppBuilder.CreateSplatBuilder()
.WithAkavache<NewtonsoftSerializer>(
() => serializer,
builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider()
.WithSqliteDefaults());
For maximum backward compatibility with existing Akavache data: use UseBsonFormat = true
in either serializer.
Akavache provides four types of caches, each with different characteristics:
Purpose: User settings and preferences that should persist and potentially sync across devices.
// Store user preferences
var settings = new UserSettings { Theme = "Dark", Language = "en-US" };
await CacheDatabase.UserAccount.InsertObject("user_settings", settings);
// Retrieve preferences
var userSettings = await CacheDatabase.UserAccount.GetObject<UserSettings>("user_settings");
Platform Behavior:
- iOS/macOS: Backed up to iCloud
- Windows: May be synced via Microsoft Account
- Android: Stored in internal app data
Purpose: Cached data that can be safely deleted by the system.
// Cache API responses
var apiData = await httpClient.GetFromJsonAsync<ApiResponse>("https://api.example.com/data");
await CacheDatabase.LocalMachine.InsertObject("api_cache", apiData, DateTimeOffset.Now.AddHours(6));
// Retrieve with fallback
var cachedData = await CacheDatabase.LocalMachine.GetOrFetchObject("api_cache",
() => httpClient.GetFromJsonAsync<ApiResponse>("https://api.example.com/data"));
Platform Behavior:
- iOS: May be deleted by system when storage is low
- Android: Subject to cache cleanup policies
- Windows/macOS: Stored in temp/cache directories
Purpose: Encrypted storage for sensitive data like credentials and API keys.
// Store credentials
await CacheDatabase.Secure.SaveLogin("john.doe", "secretPassword", "myapp.com");
// Retrieve credentials
var loginInfo = await CacheDatabase.Secure.GetLogin("myapp.com");
Console.WriteLine($"User: {loginInfo.UserName}, Password: {loginInfo.Password}");
// Store API keys
await CacheDatabase.Secure.InsertObject("api_key", "sk-1234567890abcdef");
var apiKey = await CacheDatabase.Secure.GetObject<string>("api_key");
Purpose: Temporary storage that doesn't persist between app sessions.
// Cache session data
var sessionData = new SessionInfo { UserId = 123, SessionToken = "abc123" };
await CacheDatabase.InMemory.InsertObject("current_session", sessionData);
// Fast temporary storage
await CacheDatabase.InMemory.InsertObject("temp_calculation", expensiveResult);
// Store simple objects
await CacheDatabase.UserAccount.InsertObject("key", myObject);
// Store with expiration
await CacheDatabase.LocalMachine.InsertObject("temp_key", data, DateTimeOffset.Now.AddMinutes(30));
// Store multiple objects
var keyValuePairs = new Dictionary<string, MyData>
{
["key1"] = new MyData { Value = 1 },
["key2"] = new MyData { Value = 2 }
};
await CacheDatabase.UserAccount.InsertObjects(keyValuePairs);
// Store raw bytes
await CacheDatabase.LocalMachine.Insert("raw_key", Encoding.UTF8.GetBytes("Hello World"));
// Get single object
var data = await CacheDatabase.UserAccount.GetObject<MyData>("key");
// Get multiple objects
var keys = new[] { "key1", "key2", "key3" };
var results = await CacheDatabase.UserAccount.GetObjects<MyData>(keys).ToList();
// Get all objects of a type
var allData = await CacheDatabase.UserAccount.GetAllObjects<MyData>().ToList();
// Get raw bytes
var rawData = await CacheDatabase.LocalMachine.Get("raw_key");
// Handle missing keys
try
{
var data = await CacheDatabase.UserAccount.GetObject<MyData>("nonexistent_key");
}
catch (KeyNotFoundException)
{
// Key not found
var defaultData = new MyData();
}
// Use fallback pattern
var data = await CacheDatabase.UserAccount.GetObject<MyData>("key")
.Catch(Observable.Return(new MyData()));
// โ
RECOMMENDED: Use existing invalidation methods
await CacheDatabase.UserAccount.Invalidate("key"); // Remove any key
await CacheDatabase.UserAccount.InvalidateObject<MyData>("key"); // Remove typed key (recommended)
await CacheDatabase.UserAccount.Invalidate(new[] { "key1", "key2" }); // Remove multiple keys
await CacheDatabase.UserAccount.InvalidateObjects<MyData>(new[] { "key1", "key2" }); // Remove multiple typed keys
// Remove all objects of a type
await CacheDatabase.UserAccount.InvalidateAllObjects<MyData>();
// Remove all data
await CacheDatabase.UserAccount.InvalidateAll();
Best Practices:
- โ
Use
InvalidateObject<T>()
methods for type-safe deletion - โ
Use
GetAllKeysSafe()
for exception-safe key enumeration in reactive chains -
โ ๏ธ Avoid complexGetAllKeys().Subscribe()
patterns - use direct invalidation instead - See Cache Deletion Guide for detailed examples
๐ง Important Fix in V11.1.1+: Prior to V11.1.1, calling
Invalidate()
on InMemory cache didn't properly clear the RequestCache, causing subsequentGetOrFetchObject
calls to return stale data instead of fetching fresh data. This has been fixed to ensure proper cache invalidation behavior. For comprehensive invalidation patterns and examples, seeCacheInvalidationPatterns.cs
.
// Extend expiration for a single cache entry
await CacheDatabase.LocalMachine.UpdateExpiration("api_data", DateTimeOffset.Now.AddHours(2));
// Extend expiration using relative time
await CacheDatabase.LocalMachine.UpdateExpiration("user_session", TimeSpan.FromMinutes(30));
// Update expiration for multiple entries
var keys = new[] { "cache_key1", "cache_key2", "cache_key3" };
await CacheDatabase.LocalMachine.UpdateExpiration(keys, DateTimeOffset.Now.AddDays(1));
๐ก For comprehensive UpdateExpiration patterns and use cases, see
UpdateExpirationPatterns.cs
in the samples directory.
The most common pattern for caching remote data:
// Basic get-or-fetch
var userData = await CacheDatabase.LocalMachine.GetOrFetchObject("user_profile",
async () => await apiClient.GetUserProfile(userId));
// With expiration
var weatherData = await CacheDatabase.LocalMachine.GetOrFetchObject("weather",
async () => await weatherApi.GetCurrentWeather(),
DateTimeOffset.Now.AddMinutes(30));
// With custom fetch observable
var liveData = await CacheDatabase.LocalMachine.GetOrFetchObject("live_data",
() => Observable.Interval(TimeSpan.FromSeconds(5))
.Select(_ => DateTime.Now.ToString()));
Returns cached data immediately, then fetches fresh data. This is one of the most powerful patterns in Akavache but requires careful handling of the dual subscription behavior.
โ ๏ธ Important: Always useSubscribe()
with GetAndFetchLatest - neverawait
it directly. The method is designed to call your subscriber twice: once with cached data (if available) and once with fresh data.
๐ก Empty Cache Behavior: When no cached data exists (first app run, after cache clear, or expired data), GetAndFetchLatest will call your subscriber once with fresh data from the fetch function. This ensures reliable data delivery even in empty cache scenarios.
// Basic usage - subscriber called 1-2 times
CacheDatabase.LocalMachine.GetAndFetchLatest("news_feed",
() => newsApi.GetLatestNews())
.Subscribe(news =>
{
// This will be called:
// - Once with fresh data (if no cached data exists)
// - Twice: cached data immediately + fresh data (if cached data exists)
UpdateUI(news);
});
Best for data where you want to completely replace the UI content:
// Simple replacement - just update the UI each time
CacheDatabase.LocalMachine.GetAndFetchLatest("user_profile",
() => userApi.GetProfile(userId))
.Subscribe(userProfile =>
{
// Replace entire UI content - works for both cached and fresh data
DisplayUserProfile(userProfile);
// Optional: Show loading indicator only on fresh data
if (IsLoadingFreshData())
{
HideLoadingIndicator();
}
});
Best for lists where you want to merge new items with existing ones:
public class MessageService
{
private readonly List<Message> _currentMessages = new();
private bool _isFirstLoad = true;
public IObservable<List<Message>> GetMessages(int ticketId)
{
return CacheDatabase.LocalMachine.GetAndFetchLatest($"messages_{ticketId}",
() => messageApi.GetMessages(ticketId))
.Do(messages =>
{
if (_isFirstLoad)
{
// First call: load cached data or initial fresh data
_currentMessages.Clear();
_currentMessages.AddRange(messages);
_isFirstLoad = false;
}
else
{
// Second call: merge fresh data with existing
var newMessages = messages.Except(_currentMessages, new MessageComparer()).ToList();
_currentMessages.AddRange(newMessages);
// Optional: Sort by timestamp
_currentMessages.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp));
}
})
.Select(_ => _currentMessages.ToList()); // Return defensive copy
}
}
Best for complex scenarios where you need fine-grained control:
public class NewsService
{
private readonly Subject<List<NewsItem>> _newsSubject = new();
private List<NewsItem> _cachedNews = new();
private bool _hasCachedData = false;
public IObservable<List<NewsItem>> GetNews()
{
CacheDatabase.LocalMachine.GetAndFetchLatest("news_feed",
() => newsApi.GetLatestNews())
.Subscribe(freshNews =>
{
if (!_hasCachedData)
{
// First emission: cached data (or first fresh data if no cache)
_cachedNews = freshNews.ToList();
_hasCachedData = true;
_newsSubject.OnNext(_cachedNews);
}
else
{
// Second emission: fresh data - perform smart merge
var updatedItems = new List<NewsItem>();
var newItems = new List<NewsItem>();
foreach (var freshItem in freshNews)
{
var existingItem = _cachedNews.FirstOrDefault(c => c.Id == freshItem.Id);
if (existingItem != null)
{
// Update existing item if content changed
if (existingItem.LastModified < freshItem.LastModified)
{
updatedItems.Add(freshItem);
var index = _cachedNews.IndexOf(existingItem);
_cachedNews[index] = freshItem;
}
}
else
{
// New item
newItems.Add(freshItem);
_cachedNews.Add(freshItem);
}
}
// Remove items that no longer exist
_cachedNews.RemoveAll(cached => !freshNews.Any(fresh => fresh.Id == cached.Id));
// Notify subscribers with current state
_newsSubject.OnNext(_cachedNews.ToList());
// Optional: Emit specific change notifications
if (newItems.Any()) OnNewItemsAdded?.Invoke(newItems);
if (updatedItems.Any()) OnItemsUpdated?.Invoke(updatedItems);
}
});
return _newsSubject.AsObservable();
}
}
Best for providing responsive UI feedback:
public class DataService
{
public IObservable<DataState<List<Product>>> GetProducts()
{
var loadingState = Observable.Return(DataState<List<Product>>.Loading());
var dataStream = CacheDatabase.LocalMachine.GetAndFetchLatest("products",
() => productApi.GetProducts())
.Select(products => DataState<List<Product>>.Success(products))
.Catch<DataState<List<Product>>, Exception>(ex =>
Observable.Return(DataState<List<Product>>.Error(ex)));
return loadingState.Concat(dataStream);
}
}
// Usage in ViewModel
public class ProductViewModel
{
public ProductViewModel()
{
_dataService.GetProducts()
.Subscribe(state =>
{
switch (state.Status)
{
case DataStatus.Loading:
IsLoading = true;
break;
case DataStatus.Success:
IsLoading = false;
Products = state.Data;
break;
case DataStatus.Error:
IsLoading = false;
ErrorMessage = state.Error?.Message;
break;
}
});
}
}
Control when fresh data should be fetched:
// Only fetch fresh data if cached data is older than 5 minutes
CacheDatabase.LocalMachine.GetAndFetchLatest("weather_data",
() => weatherApi.GetCurrentWeather(),
fetchPredicate: cachedDate => DateTimeOffset.Now - cachedDate > TimeSpan.FromMinutes(5))
.Subscribe(weather => UpdateWeatherDisplay(weather));
// Fetch fresh data based on user preference
CacheDatabase.LocalMachine.GetAndFetchLatest("user_settings",
() => settingsApi.GetUserSettings(),
fetchPredicate: _ => userPreferences.AllowBackgroundRefresh)
.Subscribe(settings => ApplySettings(settings));
// โ DON'T: Await GetAndFetchLatest - you'll only get first result
var data = await CacheDatabase.LocalMachine.GetAndFetchLatest("key", fetchFunc).FirstAsync();
// โ DON'T: Mix cached retrieval with GetAndFetchLatest
var cached = await cache.GetObject<T>("key").FirstOrDefaultAsync();
cache.GetAndFetchLatest("key", fetchFunc).Subscribe(fresh => /* handle fresh */);
// โ DON'T: Ignore the dual nature in UI updates
cache.GetAndFetchLatest("key", fetchFunc)
.Subscribe(data => items.Clear()); // This will clear twice!
- Always use Subscribe(), never await - GetAndFetchLatest is designed for reactive scenarios
- Handle both cached and fresh data appropriately - Design your subscriber to work correctly when called 1-2 times (once if no cache, twice if cached data exists)
- Use state tracking for complex merges - Keep track of whether you're handling cached or fresh data
- Provide loading indicators - Show users when fresh data is being fetched
- Handle errors gracefully - Network calls can fail, always have fallback logic
- Consider using fetchPredicate - Avoid unnecessary network calls when cached data is still fresh
- Test empty cache scenarios - Ensure your app works correctly on first run or after cache clears
// Download and cache URLs
var imageData = await CacheDatabase.LocalMachine.DownloadUrl("https://example.com/image.jpg");
// With custom headers
var headers = new Dictionary<string, string>
{
["Authorization"] = "Bearer " + token,
["User-Agent"] = "MyApp/1.0"
};
var apiResponse = await CacheDatabase.LocalMachine.DownloadUrl("https://api.example.com/data",
HttpMethod.Get, headers);
// Force fresh download
var freshData = await CacheDatabase.LocalMachine.DownloadUrl("https://api.example.com/live",
fetchAlways: true);
// Save login credentials (encrypted)
await CacheDatabase.Secure.SaveLogin("username", "password", "myapp.com");
// Retrieve credentials
var loginInfo = await CacheDatabase.Secure.GetLogin("myapp.com");
Console.WriteLine($"User: {loginInfo.UserName}");
// Multiple hosts
await CacheDatabase.Secure.SaveLogin("user1", "pass1", "api.service1.com");
await CacheDatabase.Secure.SaveLogin("user2", "pass2", "api.service2.com");
Akavache provides UpdateExpiration
methods that efficiently update cache entry expiration dates without reading or writing the cached data. This is particularly useful for HTTP caching scenarios and session management.
- High Performance: Only updates metadata, leaving cached data untouched
- SQL Efficiency: Uses targeted UPDATE statements rather than full record replacement
- Bulk Operations: Update multiple entries in a single transaction
- No Data Transfer: Avoids expensive serialization/deserialization cycles (up to 250x faster)
// Single entry with absolute expiration
await cache.UpdateExpiration("api_response", DateTimeOffset.Now.AddHours(6));
// Single entry with relative time
await cache.UpdateExpiration("user_session", TimeSpan.FromMinutes(30));
// Bulk update multiple entries
var keys = new[] { "weather_seattle", "weather_portland", "weather_vancouver" };
await cache.UpdateExpiration(keys, TimeSpan.FromHours(2));
// HTTP 304 Not Modified response handling
if (response.StatusCode == HttpStatusCode.NotModified)
{
await cache.UpdateExpiration(cacheKey, TimeSpan.FromHours(1));
return cachedData; // Serve existing data with extended lifetime
}
๐ For comprehensive patterns and real-world examples, see
UpdateExpirationPatterns.cs
, which includes:
- HTTP caching with 304 Not Modified handling
- Session management with sliding expiration
- Bulk operations and performance optimization
- Error handling and best practices
- Performance comparisons and method overload reference
// Cache for relative time periods
await CacheDatabase.LocalMachine.InsertObject("data", myData, TimeSpan.FromMinutes(30).FromNow());
// Use in get-or-fetch
var cachedData = await CacheDatabase.LocalMachine.GetOrFetchObject("api_data",
() => FetchFromApi(),
1.Hours().FromNow());
// Use custom scheduler for background operations
CacheDatabase.TaskpoolScheduler = TaskPoolScheduler.Default;
// Or use a custom scheduler
CacheDatabase.TaskpoolScheduler = new EventLoopScheduler();
// Get all keys (for debugging)
var allKeys = await CacheDatabase.UserAccount.GetAllKeys().ToList();
// Safe key enumeration with exception handling in observable chain
var safeKeys = await CacheDatabase.UserAccount.GetAllKeysSafe().ToList();
// GetAllKeysSafe catches exceptions and continues the observable chain
// instead of throwing - useful for robust error handling
// Get keys for specific types safely
var typedKeys = await CacheDatabase.UserAccount.GetAllKeysSafe<MyDataType>().ToList();
var specificTypeKeys = await CacheDatabase.UserAccount.GetAllKeysSafe(typeof(string)).ToList();
// Check when item was created
var createdAt = await CacheDatabase.UserAccount.GetCreatedAt("my_key");
if (createdAt.HasValue)
{
Console.WriteLine($"Item created at: {createdAt.Value}");
}
// Get creation times for multiple keys
var creationTimes = await CacheDatabase.UserAccount.GetCreatedAt(new[] { "key1", "key2" })
.ToList();
The GetAllKeysSafe
methods provide exception-safe alternatives to GetAllKeys()
that handle errors within the observable chain:
// Standard GetAllKeys() - exceptions break the observable chain
try
{
var keys = await CacheDatabase.UserAccount.GetAllKeys().ToList();
// Process keys...
}
catch (Exception ex)
{
// Handle exception outside observable chain
}
// GetAllKeysSafe() - exceptions are caught and logged, chain continues
await CacheDatabase.UserAccount.GetAllKeysSafe()
.Do(key => Console.WriteLine($"Found key: {key}"))
.Where(key => ShouldProcess(key))
.ForEach(key => ProcessKey(key));
// If GetAllKeys() would throw, this continues with empty sequence instead
Key differences:
- Exception handling: Catches exceptions and returns empty sequence instead of throwing
- Null safety: Filters out null or empty keys automatically
- Observable chain friendly: Allows reactive code to continue executing even when underlying storage has issues
- Logging: Logs exceptions for debugging while keeping the application stable
Use GetAllKeysSafe when:
- Building reactive pipelines that should be resilient to storage exceptions
- You want exceptions handled within the observable chain rather than breaking it
- Working with unreliable storage scenarios or during development/testing
- You prefer continuation over immediate failure when key enumeration fails
// Force flush all pending operations
await CacheDatabase.UserAccount.Flush();
// Vacuum database (SQLite only - removes deleted data)
await CacheDatabase.UserAccount.Vacuum();
// Flush specific object type
await CacheDatabase.UserAccount.Flush(typeof(MyDataType));
// Store different types with one operation
var mixedData = new Dictionary<string, object>
{
["string_data"] = "Hello World",
["number_data"] = 42,
["object_data"] = new MyClass { Value = "test" },
["date_data"] = DateTime.Now
};
await CacheDatabase.UserAccount.InsertObjects(mixedData);
Akavache.Drawing provides comprehensive image caching and bitmap manipulation functionality for Akavache applications. Built on Splat, it offers cross-platform support for loading, caching, and manipulating images with enhanced features beyond basic blob storage.
- Image Loading & Caching: Load images from cache with automatic format detection
- URL Image Caching: Download and cache images from URLs with built-in HTTP support
- Image Manipulation: Resize, crop, and generate thumbnails with caching
- Multiple Format Support: PNG, JPEG, GIF, BMP, WebP, and other common formats
- Fallback Support: Automatic fallback to default images when loading fails
- Batch Operations: Load multiple images efficiently
- Size Detection: Get image dimensions without full loading
- Advanced Caching: Pattern-based cache clearing and preloading
- Cross-Platform: Works on all .NET platforms supported by Akavache
<PackageReference Include="Akavache.Drawing" Version="11.1.*" />
Akavache.Drawing requires:
-
Akavache.Core
- Core caching functionality -
Splat.Drawing
- Cross-platform bitmap abstractions
using Akavache.Core;
using Akavache.Drawing;
using Akavache.SystemTextJson;
using Splat;
// Initialize Akavache with drawing support
CacheDatabase.Initialize<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyImageApp")
.WithSqliteProvider()
.WithSqliteDefaults());
// Register platform-specific bitmap loader using Splat (if needed (Net 8.0+))
AppLocator.CurrentMutable.RegisterPlatformBitmapLoader();
// Load image from cache
var image = await CacheDatabase.LocalMachine.LoadImage("user_avatar");
// Load with custom sizing
var thumbnail = await CacheDatabase.LocalMachine.LoadImage("user_avatar", 150, 150);
// Load with error handling
try
{
var profileImage = await CacheDatabase.UserAccount.LoadImage("profile_pic");
DisplayImage(profileImage);
}
catch (KeyNotFoundException)
{
// Image not found in cache
ShowDefaultImage();
}
// Download and cache image from URL
var imageFromUrl = await CacheDatabase.LocalMachine
.LoadImageFromUrl("https://example.com/images/photo.jpg");
// With custom expiration
var tempImage = await CacheDatabase.LocalMachine
.LoadImageFromUrl("https://api.example.com/temp-image.png",
absoluteExpiration: DateTimeOffset.Now.AddHours(1));
// Force fresh download (bypass cache)
var freshImage = await CacheDatabase.LocalMachine
.LoadImageFromUrl("https://api.example.com/live-feed.jpg", fetchAlways: true);
// With custom key
var namedImage = await CacheDatabase.LocalMachine
.LoadImageFromUrl("user_background", "https://example.com/bg.jpg");
// Save image to cache
await CacheDatabase.LocalMachine.SaveImage("user_photo", bitmap);
// Save with expiration
await CacheDatabase.LocalMachine.SaveImage("temp_image", bitmap,
DateTimeOffset.Now.AddDays(7));
// Convert bitmap to bytes for manual storage
var imageBytes = await bitmap.ImageToBytes().FirstAsync();
await CacheDatabase.LocalMachine.Insert("raw_image_data", imageBytes);
// Load multiple images at once
var imageKeys = new[] { "image1", "image2", "image3" };
var loadedImages = await CacheDatabase.LocalMachine
.LoadImages(imageKeys, desiredWidth: 200, desiredHeight: 200)
.ToList();
foreach (var kvp in loadedImages)
{
Console.WriteLine($"Loaded {kvp.Key}: {kvp.Value.Width}x{kvp.Value.Height}");
}
// Preload images from URLs (background caching)
var urls = new[]
{
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
"https://example.com/image3.jpg"
};
await CacheDatabase.LocalMachine.PreloadImagesFromUrls(urls,
DateTimeOffset.Now.AddDays(1));
// Load image with automatic fallback
var defaultImageBytes = File.ReadAllBytes("default-avatar.png");
var userAvatar = await CacheDatabase.UserAccount
.LoadImageWithFallback("user_avatar", defaultImageBytes, 100, 100);
// Load from URL with fallback
var profileImage = await CacheDatabase.LocalMachine
.LoadImageFromUrlWithFallback("https://example.com/profile.jpg",
defaultImageBytes,
desiredWidth: 200,
desiredHeight: 200);
// Create and cache thumbnail from existing image
await CacheDatabase.LocalMachine.CreateAndCacheThumbnail(
sourceKey: "original_photo",
thumbnailKey: "photo_thumb",
thumbnailWidth: 150,
thumbnailHeight: 150,
absoluteExpiration: DateTimeOffset.Now.AddDays(30));
// Load the cached thumbnail
var thumbnail = await CacheDatabase.LocalMachine.LoadImage("photo_thumb");
// Get image dimensions without fully loading
var imageSize = await CacheDatabase.LocalMachine.GetImageSize("large_image");
Console.WriteLine($"Image size: {imageSize.Width}x{imageSize.Height}");
Console.WriteLine($"Aspect ratio: {imageSize.AspectRatio:F2}");
// Use size info for layout decisions
if (imageSize.AspectRatio > 1.5)
{
// Wide image
SetWideImageLayout();
}
else
{
// Square or tall image
SetNormalImageLayout();
}
// Clear images matching a pattern
await CacheDatabase.LocalMachine.ClearImageCache(key => key.StartsWith("temp_"));
// Clear all user avatars
await CacheDatabase.UserAccount.ClearImageCache(key => key.Contains("avatar"));
// Clear expired images
await CacheDatabase.LocalMachine.ClearImageCache(key =>
key.StartsWith("cache_") && IsExpired(key));
public class PhotoGalleryService
{
private readonly IBlobCache _imageCache;
private readonly IBlobCache _thumbnailCache;
public PhotoGalleryService()
{
// Initialize Akavache with drawing support
AppBuilder.CreateSplatBuilder().WithAkavacheCacheDatabase<SystemJsonSerializer>(builder =>
builder.WithApplicationName("PhotoGallery")
.WithSqliteProvider() // REQUIRED: Explicit provider
.WithSqliteDefaults());
_imageCache = CacheDatabase.LocalMachine;
_thumbnailCache = CacheDatabase.UserAccount;
}
public async Task<IBitmap> LoadPhotoAsync(string photoId, bool generateThumbnail = false)
{
try
{
// Try to load from cache first
var photo = await _imageCache.LoadImage($"photo_{photoId}");
// Generate thumbnail if requested and not exists
if (generateThumbnail)
{
await _thumbnailCache.CreateAndCacheThumbnail(
$"photo_{photoId}",
$"thumb_{photoId}",
200, 200,
DateTimeOffset.Now.AddMonths(1));
}
return photo;
}
catch (KeyNotFoundException)
{
// Load from remote URL if not cached
var photoUrl = $"https://api.photos.com/images/{photoId}";
return await _imageCache.LoadImageFromUrl($"photo_{photoId}", photoUrl,
absoluteExpiration: DateTimeOffset.Now.AddDays(7));
}
}
public async Task<IBitmap> LoadThumbnailAsync(string photoId)
{
try
{
return await _thumbnailCache.LoadImage($"thumb_{photoId}", 200, 200);
}
catch (KeyNotFoundException)
{
// Generate thumbnail from full image
var fullImage = await LoadPhotoAsync(photoId);
await _thumbnailCache.SaveImage($"thumb_{photoId}", fullImage,
DateTimeOffset.Now.AddMonths(1));
return await _thumbnailCache.LoadImage($"thumb_{photoId}", 200, 200);
}
}
public async Task PreloadGalleryAsync(IEnumerable<string> photoIds)
{
var photoUrls = photoIds.Select(id => $"https://api.photos.com/images/{id}");
await _imageCache.PreloadImagesFromUrls(photoUrls,
DateTimeOffset.Now.AddDays(7));
}
public async Task ClearOldCacheAsync()
{
// Clear images older than 30 days
await _imageCache.ClearImageCache(key =>
key.StartsWith("photo_") && IsOlderThan30Days(key));
// Clear thumbnails older than 60 days
await _thumbnailCache.ClearImageCache(key =>
key.StartsWith("thumb_") && IsOlderThan60Days(key));
}
private static bool IsOlderThan30Days(string key) =>
/* Implementation to check cache age */ false;
private static bool IsOlderThan60Days(string key) =>
/* Implementation to check cache age */ false;
}
Akavache.Settings provides a specialized settings database for installable applications. It creates persistent settings that are stored one level down from the application folder, making application updates less painful as the settings survive reinstalls.
- Type-Safe Settings: Strongly-typed properties with default values
- Automatic Persistence: Settings are automatically saved when changed
- Application Update Friendly: Settings survive application reinstalls
- Encrypted Storage: Optional secure settings with password protection
- Multiple Settings Classes: Support for multiple settings categories
<PackageReference Include="Akavache.Settings" Version="11.1.*" />
using Akavache.Settings;
public class AppSettings : SettingsBase
{
public AppSettings() : base(nameof(AppSettings))
{
}
// Boolean setting with default value
public bool EnableNotifications
{
get => GetOrCreate(true);
set => SetOrCreate(value);
}
// String setting with default value
public string UserName
{
get => GetOrCreate("DefaultUser");
set => SetOrCreate(value);
}
// Numeric settings
public int MaxRetries
{
get => GetOrCreate(3);
set => SetOrCreate(value);
}
public double CacheTimeout
{
get => GetOrCreate(30.0);
set => SetOrCreate(value);
}
// Enum setting
public LogLevel LoggingLevel
{
get => GetOrCreate(LogLevel.Information);
set => SetOrCreate(value);
}
}
public enum LogLevel
{
Debug,
Information,
Warning,
Error
}
using Akavache.Core;
using Akavache.SystemTextJson;
using Akavache.Settings;
// Initialize Akavache with settings support
var appSettings = default(AppSettings);
AppBuilder.CreateSplatBuilder().WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSerializer(new SystemJsonSerializer())
.WithSqliteProvider()
.WithSettingsStore<AppSettings>(settings => appSettings = settings));
// Now use the settings
appSettings.EnableNotifications = false;
appSettings.UserName = "John Doe";
appSettings.MaxRetries = 5;
Console.WriteLine($"User: {appSettings.UserName}");
Console.WriteLine($"Notifications: {appSettings.EnableNotifications}");
By default, settings are stored in a subfolder of your application directory. You can customize this path:
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider()
.WithSettingsCachePath(@"C:\MyApp\Settings") // Custom path
.WithSettingsStore<AppSettings>(settings => appSettings = settings));
You can create multiple settings classes for different categories:
public class UserSettings : SettingsBase
{
public UserSettings() : base(nameof(UserSettings)) { }
public string Theme
{
get => GetOrCreate("Light");
set => SetOrCreate(value);
}
}
public class NetworkSettings : SettingsBase
{
public NetworkSettings() : base(nameof(NetworkSettings)) { }
public int TimeoutSeconds
{
get => GetOrCreate(30);
set => SetOrCreate(value);
}
}
// Initialize multiple settings
var userSettings = default(UserSettings);
var networkSettings = default(NetworkSettings);
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider()
.WithSettingsStore<UserSettings>(settings => userSettings = settings)
.WithSettingsStore<NetworkSettings>(settings => networkSettings = settings));
For sensitive settings, use encrypted storage:
public class SecureSettings : SettingsBase
{
public SecureSettings() : base(nameof(SecureSettings)) { }
public string ApiKey
{
get => GetOrCreate(string.Empty);
set => SetOrCreate(value);
}
public string DatabasePassword
{
get => GetOrCreate(string.Empty);
set => SetOrCreate(value);
}
}
// Initialize with encryption
var secureSettings = default(SecureSettings);
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithEncryptedSqliteProvider()
.WithSecureSettingsStore<SecureSettings>("mySecurePassword",
settings => secureSettings = settings));
// Use encrypted settings
secureSettings.ApiKey = "sk-1234567890abcdef";
secureSettings.DatabasePassword = "super-secret-password";
You can specify custom database names for settings:
var appSettings = default(AppSettings);
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider()
.WithSettingsStore<AppSettings>(
settings => appSettings = settings,
"CustomAppConfig")); // Custom database name
Here's a comprehensive example showing all data types and features:
public class ComprehensiveSettings : SettingsBase
{
public ComprehensiveSettings() : base(nameof(ComprehensiveSettings))
{
}
// Basic types with defaults
public bool BoolSetting
{
get => GetOrCreate(true);
set => SetOrCreate(value);
}
public byte ByteSetting
{
get => GetOrCreate((byte)123);
set => SetOrCreate(value);
}
public short ShortSetting
{
get => GetOrCreate((short)16);
set => SetOrCreate(value);
}
public int IntSetting
{
get => GetOrCreate(42);
set => SetOrCreate(value);
}
public long LongSetting
{
get => GetOrCreate(123456L);
set => SetOrCreate(value);
}
public float FloatSetting
{
get => GetOrCreate(2.5f);
set => SetOrCreate(value);
}
public double DoubleSetting
{
get => GetOrCreate(3.14159);
set => SetOrCreate(value);
}
public string StringSetting
{
get => GetOrCreate("Default Value");
set => SetOrCreate(value);
}
// Nullable types
public string? NullableStringSetting
{
get => GetOrCreate<string?>(null);
set => SetOrCreate(value);
}
// Complex types (automatically serialized)
public List<string> StringListSetting
{
get => GetOrCreate(new List<string> { "Item1", "Item2" });
set => SetOrCreate(value);
}
public Dictionary<string, int> DictionarySetting
{
get => GetOrCreate(new Dictionary<string, int> { ["Key1"] = 1, ["Key2"] = 2 });
set => SetOrCreate(value);
}
// Custom objects
public WindowPosition WindowPosition
{
get => GetOrCreate(new WindowPosition { X = 100, Y = 100, Width = 800, Height = 600 });
set => SetOrCreate(value);
}
}
public class WindowPosition
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
// Usage
var settings = default(ComprehensiveSettings);
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider()
.WithSettingsStore<ComprehensiveSettings>(s => settings = s));
// Use the settings
settings.StringListSetting.Add("Item3");
settings.WindowPosition = new WindowPosition { X = 200, Y = 150, Width = 1024, Height = 768 };
settings.DictionarySetting["NewKey"] = 999;
// In your application shutdown code
public async Task OnApplicationExit()
{
var builder = CacheDatabase.Builder;
// Dispose settings stores to ensure data is flushed
await builder.DisposeSettingsStore<AppSettings>();
await builder.DisposeSettingsStore<UserSettings>();
// Regular Akavache shutdown
await CacheDatabase.Shutdown();
}
// Delete a specific settings store
var builder = CacheDatabase.Builder;
await builder.DeleteSettingsStore<AppSettings>();
// Settings will be recreated with default values on next access
var builder = CacheDatabase.Builder;
var existingSettings = builder.GetSettingsStore<AppSettings>();
if (existingSettings != null)
{
Console.WriteLine("Settings already exist");
}
else
{
Console.WriteLine("First run - settings will be created with defaults");
}
Note: MAUI targets in this repository are documented for .NET 9 only. For older TFMs, please use a previous release/tag or consult historical docs. See MAUI .NET 9 Support Documentation for official guidance.
Supported Target Frameworks:
-
net9.0-android
- Android applications -
net9.0-ios
- iOS applications -
net9.0-maccatalyst
- Mac Catalyst applications -
net9.0-windows
- Windows applications (WinUI)
// In MauiProgram.cs
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Initialize Akavache early
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(cacheBuilder =>
cacheBuilder.WithApplicationName("MyMauiApp")
.WithSqliteProvider() // REQUIRED: Explicit provider
.WithForceDateTimeKind(DateTimeKind.Utc)
.WithSqliteDefaults());
return builder.Build();
}
}
Example Project Configuration:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
<UseMaui>true</UseMaui>
<!-- Other MAUI configuration -->
</PropertyGroup>
</Project>
// In App.xaml.cs
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
ConfigureAkavache();
base.OnStartup(e);
}
protected override void OnExit(ExitEventArgs e)
{
// Important: Shutdown Akavache properly
CacheDatabase.Shutdown().Wait();
base.OnExit(e);
}
private static void ConfigureAkavache()
{
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyWpfApp")
.WithSqliteProvider() // REQUIRED: Explicit provider
.WithForceDateTimeKind(DateTimeKind.Utc)
.WithSqliteDefaults());
}
}
// In AppDelegate.cs or SceneDelegate.cs
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyiOSApp")
.WithSqliteProvider() // REQUIRED: Explicit provider
.WithSqliteDefaults());
return base.FinishedLaunching(application, launchOptions);
}
// In MainActivity.cs or Application class
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyAndroidApp")
.WithSqliteProvider() // REQUIRED: Explicit provider
.WithSqliteDefaults());
}
// In App.xaml.cs
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyUwpApp")
.WithSqliteProvider() // REQUIRED: Explicit provider
.WithSqliteDefaults());
// Rest of initialization...
}
Important for UWP: Mark your application as x86
or ARM
, not Any CPU
.
Akavache V11.0 delivers architectural improvements with optimal performance when using the recommended System.Text.Json serializer. V11 with System.Text.Json outperforms V10 across all test scenarios, while V11 with the legacy Newtonsoft.Json may be slower than V10 for very large datasets. The new features (multiple serializers, cross-compatibility, modern patterns) provide significant value with excellent performance when using the recommended serializer.
Based on comprehensive benchmarks across different operation types and data sizes:
Operation | Small (10 items) | Medium (100 items) | Large (1000 items) | Notes |
---|---|---|---|---|
GetOrFetch | 1.5ms | 15ms | 45ms | Sub-linear scaling, excellent for cache-miss scenarios |
Bulk Operations | 3.3ms | 4.5ms | 18ms | 10x+ faster than individual operations |
In-Memory | 2.4ms | 19ms | 123ms | Ideal for session data and frequently accessed objects |
Cache Types | ~27ms | ~255ms | ~2,600ms | Consistent performance across UserAccount/LocalMachine/Secure |
- Read Performance: V11 shows 1.8-3.4% faster performance for smaller datasets with more consistent results
- Write Performance: Comparable sequential writes, with significant bulk write advantages in V11
- Memory Usage: Generally equivalent or better memory efficiency with more predictable allocation patterns
- Serialization: System.Text.Json in V11 significantly outperforms both V10 and V11 Newtonsoft serialization
System.Text.Json (Recommended for V11):
- โ Best overall performance for both small and large datasets
- โ Faster than V10 across all test scenarios
- โ Modern .NET optimization with excellent memory efficiency
Newtonsoft.Json in V11 (Legacy Compatibility):
-
โ ๏ธ Slower than V10 with large databases - V10 Newtonsoft performs better for huge datasets - โ Faster than V10 for smaller to medium datasets
- โ Compatible with existing V10 data structures
- Large Databases with Newtonsoft.Json: V10 outperforms V11 when using legacy Newtonsoft serialization with very large datasets
- Sequential Read Performance: Up to 8.6% slower than V10 specifically when using the legacy Newtonsoft.Json serializer (System.Text.Json does not have this limitation and performs better than V10)
- Linux/macOS Build: Benchmark projects and compatibility tests require Windows due to platform-specific dependencies
- Package Dependencies: More granular package structure may require careful workload management
- V11 + System.Text.Json: Best performance choice - faster than V10 across all scenarios without any performance limitations
- V11 + Newtonsoft.Json (Legacy): Maximum compatibility with existing V10 data, but slower for large datasets compared to V10
- Cross-Version Compatibility: V11 can read V10 databases; subsequent writes are stored in V11 format
- BSON Format: When using Newtonsoft.Bson, reads and writes follow the V10 format for maximum compatibility and performance parity
For comprehensive performance analysis and V10 vs V11 comparison:
- ๐ Performance Summary - Quick comparison and migration decision matrix
- ๐ Comprehensive Benchmark Report - Detailed performance analysis, architectural differences, and recommendations
Platform Requirements: Benchmark reproduction requires Windows hosts. Linux/macOS are not supported due to Windows-specific projects and dependencies used in the benchmark harnesses.
Prerequisites:
- .NET 9.0 SDK
- Windows operating system
- PowerShell 5.0+ (for automation script)
Test Applications:
- AkavacheV10Writer - Writes deterministic test data using Akavache V10 with Newtonsoft.Json serialization
- AkavacheV11Reader - Reads the same data using Akavache V11 with System.Text.Json, demonstrating cross-version compatibility
Running Compatibility Tests:
# From the solution root directory
.\src\RunCompatTest.ps1
This PowerShell script:
- Builds both test applications in Release configuration
- Runs AkavacheV10Writer to create a test database
- Runs AkavacheV11Reader to verify cross-compatibility
- Reports success/failure of the compatibility verification
Running Performance Benchmarks:
# V11 benchmarks (current)
cd src
dotnet run -c Release -p Akavache.Benchmarks/Akavache.Benchmarks.csproj
# V10 comparison benchmarks
dotnet run -c Release -p Akavache.Benchmarks.V10/Akavache.Benchmarks.V10.csproj
Important Notes:
- Results vary by hardware configuration and system load
- Benchmarks are indicative, not absolute measurements
- Large benchmark runs can take 10-30 minutes to complete
- Some benchmark projects use BenchmarkDotNet which requires Windows-specific optimizations
// 1. ALWAYS use System.Text.Json for optimal V11 performance
// This is faster than V10 across all scenarios and significantly faster than V11 Newtonsoft
.WithSerializer<SystemJsonSerializer>();
// 2. For V10 compatibility with large datasets, consider Newtonsoft BSON
// (Only if you need V10 format compatibility - otherwise use System.Text.Json)
.WithSerializer<NewtonsoftBsonSerializer>();
// 3. Use batch operations for multiple items
await CacheDatabase.UserAccount.InsertObjects(manyItems);
// 4. Set appropriate expiration times
await CacheDatabase.LocalMachine.InsertObject("temp", data, 30.Minutes().FromNow());
// 5. Use InMemory cache for frequently accessed data
await CacheDatabase.InMemory.InsertObject("hot_data", frequentData);
// 5. Avoid storing very large objects
// Instead, break them into smaller chunks or use compression
// 6. Use specific types instead of object when possible
await CacheDatabase.UserAccount.GetObject<SpecificType>("key"); // Good
await CacheDatabase.UserAccount.Get("key", typeof(SpecificType)); // Slower
// โ
Do: Initialize once at app startup
public class App
{
static App()
{
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider() // REQUIRED: Explicit provider
.WithSqliteDefaults());
}
}
// โ Don't: Initialize multiple times
// โ
Do: Use consistent, descriptive key naming
await CacheDatabase.UserAccount.InsertObject("user_profile_123", userProfile);
await CacheDatabase.LocalMachine.InsertObject("api_cache_weather_seattle", weatherData);
// โ
Do: Use constants for keys
public static class CacheKeys
{
public const string UserProfile = "user_profile";
public const string WeatherData = "weather_data";
}
// โ Don't: Use random or inconsistent keys
await CacheDatabase.UserAccount.InsertObject("xyz123", someData);
// โ
Do: Handle KeyNotFoundException appropriately
try
{
var data = await CacheDatabase.UserAccount.GetObject<MyData>("key");
}
catch (KeyNotFoundException)
{
// Provide fallback or default behavior
var defaultData = new MyData();
}
// โ
Do: Use GetOrFetchObject for remote data
var data = await CacheDatabase.LocalMachine.GetOrFetchObject("api_data",
() => httpClient.GetFromJsonAsync<ApiData>("https://api.example.com/data"));
// โ
Do: Use appropriate cache types
await CacheDatabase.UserAccount.InsertObject("user_settings", settings); // Persistent user data
await CacheDatabase.LocalMachine.InsertObject("api_cache", apiData); // Cacheable data
await CacheDatabase.Secure.InsertObject("api_key", apiKey); // Sensitive data
await CacheDatabase.InMemory.InsertObject("session_data", sessionData); // Temporary data
// โ
Do: Set appropriate expiration times
await CacheDatabase.LocalMachine.InsertObject("api_data", data, 1.Hours().FromNow());
await CacheDatabase.LocalMachine.InsertObject("image_cache", imageBytes, 1.Days().FromNow());
// โ
Do: Don't expire user settings (unless necessary)
await CacheDatabase.UserAccount.InsertObject("user_preferences", prefs); // No expiration
// โ
Do: Always shutdown Akavache properly
public override void OnExit(ExitEventArgs e)
{
CacheDatabase.Shutdown().Wait();
base.OnExit(e);
}
// For MAUI/Xamarin apps
protected override void OnSleep()
{
CacheDatabase.Shutdown().Wait();
base.OnSleep();
}
// โ
Do: Use in-memory cache for unit tests
[SetUp]
public void Setup()
{
CacheDatabase.Initialize<SystemJsonSerializer>(builder =>
builder.WithApplicationName("TestApp")
.WithInMemoryDefaults());
}
[TearDown]
public void TearDown()
{
CacheDatabase.Shutdown().Wait();
}
// Fix: Register a suitable serializer during initialization
CacheDatabase.Initialize<SystemJsonSerializer>(/* ... */);
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(/* ... */);
AppBuilder.CreateSplatBuilder()
.WithAkavacheCacheDatabase<SystemJsonSerializer>(/* ... */);
// Fix: Call Initialize before using cache
CacheDatabase.Initialize<SystemJsonSerializer>(builder => builder.WithApplicationName("MyApp").WithInMemoryDefaults());
var data = await CacheDatabase.UserAccount.GetObject<MyData>("key");
// Fix: Use cross-compatible serializer or migration
CacheDatabase.Initialize<NewtonsoftBsonSerializer>(/* ... */); // Most compatible
Android DllNotFoundException with SQLitePCLRaw.lib.e_sqlite3:
If you're getting System.DllNotFoundException: 'e_sqlite3'
when using SQLitePCLRaw.lib.e_sqlite3
on Android, use the appropriate bundle instead:
<!-- For Android (recommended): Use bundle_e_sqlite3 instead of lib.e_sqlite3 -->
<ItemGroup>
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.11" />
</ItemGroup>
<!-- Alternative: Use bundle_green for cross-platform compatibility -->
<ItemGroup>
<PackageReference Include="SQLitePCLRaw.bundle_green" Version="2.1.11" />
</ItemGroup>
<!-- If using Encrypted SQLite, also add: -->
<ItemGroup>
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlcipher" Version="2.1.11" />
</ItemGroup>
Platform-specific bundle recommendations:
-
Android:
SQLitePCLRaw.bundle_e_sqlite3
orSQLitePCLRaw.bundle_green
-
iOS:
SQLitePCLRaw.bundle_e_sqlite3
orSQLitePCLRaw.bundle_green
-
Desktop/Server:
SQLitePCLRaw.bundle_e_sqlite3
works fine
Note: SQLitePCLRaw.lib.e_sqlite3
is a low-level library that requires additional platform-specific setup. The bundles include the necessary native libraries and initialization code for each platform.
You will need to preserve certain types to prevent the linker from stripping them out in release builds.
// Add to your .csproj file:
<ItemGroup>
<TrimmerRootAssembly Include="SQLitePCLRaw.lib.e_sqlite3.## YOUR-PLATFORM ##" RootMode="All" />
</ItemGroup>
### Platform-Specific Issues
#### iOS Linker Issues
```csharp
// Add LinkerPreserve.cs to your iOS project:
public static class LinkerPreserve
{
static LinkerPreserve()
{
var sqliteBlobCachetName = typeof(SqliteBlobCache).FullName;
var encryptedSqliteBlobCacheName = typeof(EncryptedSqliteBlobCache).FullName;
}
}
Ensure you have the appropriate SQLitePCLRaw bundle installed for your platform:
// For general use
.WithSqliteProvider()
// For encrypted databases
.WithEncryptedSqliteProvider()
Problem: Calling Invalidate()
followed by GetOrFetchObject()
returns old data instead of fetching fresh data.
Root Cause: In versions prior to V11.1.1, Invalidate()
on InMemory cache didn't clear the RequestCache, causing GetOrFetchObject
to return cached request results.
// โ This pattern failed in pre-V11.1.1 versions
var data1 = await cache.GetOrFetchObject("key", () => FetchFromApi()); // Returns "value_1"
await cache.Invalidate("key");
var data2 = await cache.GetOrFetchObject("key", () => FetchFromApi()); // Should return "value_2" but returned "value_1"
Solution:
- Upgrade to V11.1.1+ - The bug is completely fixed
-
For older versions: Use
GetObject
+InsertObject
pattern instead ofGetOrFetchObject
after invalidation
// โ
Workaround for older versions
try
{
var data = await cache.GetObject<MyData>("key");
// Data exists, use it
}
catch (KeyNotFoundException)
{
// Data doesn't exist, fetch and store
var freshData = await FetchFromApi();
await cache.InsertObject("key", freshData);
}
Verification: See CacheInvalidationPatterns.cs
for comprehensive test patterns to verify this behavior.
Ensure your UWP project targets a specific platform (x86, x64, ARM) rather than "Any CPU".
- ๐ Documentation: https://github.com/reactiveui/Akavache
- ๐ Issues: GitHub Issues
- ๐ฌ Chat: ReactiveUI Slack
- ๐ฆ NuGet: Akavache Packages
This project is tested with BrowserStack.
Akavache is licensed under the MIT License.