Akavache 11.3.3
Prefix Reserveddotnet add package Akavache --version 11.3.3
NuGet\Install-Package Akavache -Version 11.3.3
<PackageReference Include="Akavache" Version="11.3.3" />
<PackageVersion Include="Akavache" Version="11.3.3" />
<PackageReference Include="Akavache" />
paket add Akavache --version 11.3.3
#r "nuget: Akavache, 11.3.3"
#:package Akavache@11.3.3
#addin nuget:?package=Akavache&version=11.3.3
#tool nuget:?package=Akavache&version=11.3.3
<br>
<a href="https://www.nuget.org/packages/akavache.sqlite3">
<img src="https://img.shields.io/nuget/dt/akavache.sqlite3.svg">
</a>
<a href="#backers">
<img src="https://opencollective.com/reactiveui/backers/badge.svg">
</a>
<a href="#sponsors">
<img src="https://opencollective.com/reactiveui/sponsors/badge.svg">
</a>
<a href="https://reactiveui.net/slack">
<img src="https://img.shields.io/badge/chat-slack-blue.svg">
</a>
<img alt="Akavache" src="https://raw.githubusercontent.com/reactiveui/styleguide/master/logo_akavache/main.png" width="150" />
Akavache V11.1: 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. This project is tested with BrowserStack.
What's New in V11.1
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
Development History
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.
Table of Contents
- Quick Start
- Installation
- Migration from V10.x
- Configuration
- Serializers
- Cache Types
- Basic Operations
- Extension Methods
- Advanced Features
- Platform-Specific Notes
- Performance
- Best Practices
Quick Start
1. Install Packages
<PackageReference Include="Akavache.Sqlite3" Version="11.1.*" />
<PackageReference Include="Akavache.SystemTextJson" Version="11.1.*" />
2. Initialize Akavache
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.
Static Initialization (Recommended for most apps)
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.
Dependency Injection Registration (for 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));
Manual Instance Creation (advanced scenarios)
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.
3. Use the Cache
// 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"));
Installation
Akavache V11.1 uses a modular package structure. Choose the packages that match your needs:
Core Package (Included with Serializers, In Memory only)
<PackageReference Include="Akavache" Version="11.1.**" />
Sqlite Storage Backends (recommended)
<PackageReference Include="Akavache.Sqlite3" Version="11.1.**" />
<PackageReference Include="Akavache.EncryptedSqlite3" Version="11.1.**" />
Serializers (Choose One (Required!))
<PackageReference Include="Akavache.SystemTextJson" Version="11.1.**" />
<PackageReference Include="Akavache.NewtonsoftJson" Version="11.1.**" />
Optional Extensions
<PackageReference Include="Akavache.Drawing" Version="11.1.**" />
<PackageReference Include="Akavache.Settings" Version="11.1.**" />
Migration from V10.x
Breaking Changes
- 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
Migration Steps
Old V10.x Code:
// 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);
New V11.1 Code:
// 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);
Migration Helper
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");
Configuration
Builder Pattern
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
Provider Initialization Pattern
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.
Configuration Options
1. In-Memory Only (for testing or non retensive applications)
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("TestApp")
.WithInMemoryDefaults());
2. SQLite Persistence
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider() // REQUIRED: Must be called before WithSqliteDefaults()
.WithSqliteDefaults());
3. Encrypted SQLite
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithEncryptedSqliteProvider() // REQUIRED: Must be called before WithSqliteDefaults()
.WithSqliteDefaults("mySecretPassword"));
4. Custom Cache Instances
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()));
5. DateTime Handling
// Set global DateTime behavior
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(builder =>
builder.WithApplicationName("MyApp")
.WithSqliteProvider() // REQUIRED: Provider initialization
.WithForcedDateTimeKind(DateTimeKind.Utc)
.WithSqliteDefaults());
Serializers
Akavache V11.1 supports multiple serialization formats with automatic cross-compatibility.
System.Text.Json (Recommended)
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
}
};
Newtonsoft.Json (Maximum Compatibility)
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());
BSON Variants
For maximum backward compatibility with existing Akavache data: use UseBsonFormat = true
in either serializer.
Cache Types
Akavache provides four types of caches, each with different characteristics:
UserAccount Cache
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
LocalMachine Cache
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
Secure Cache
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");
InMemory Cache
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);
Basic Operations
Storing Data
// 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"));
Retrieving Data
// 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");
Error Handling
// 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()));
Removing Data
// โ
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 complex
GetAllKeys().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
.
Updating Expiration
// 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.
Extension Methods
Get or Fetch Pattern
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()));
Get and Fetch Latest
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 use
Subscribe()
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 Pattern
// 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);
});
Pattern 1: Simple Replacement (Most Common)
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();
}
});
Pattern 2: Merge Strategy for Collections
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
}
}
Pattern 3: Differential Updates with State Tracking
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();
}
}
Pattern 4: UI Loading States
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;
}
});
}
}
Pattern 5: Conditional Fetching
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));
Common Anti-Patterns โ
// โ 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!
Best Practices โ
- 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
HTTP/URL Operations
// 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);
Login/Credential Management
// 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");
Advanced Features
Efficient Expiration Updates
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.
Key Benefits
- 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)
Quick Examples
// 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
Relative Time Extensions
// 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());
Custom Schedulers
// Use custom scheduler for background operations
CacheDatabase.TaskpoolScheduler = TaskPoolScheduler.Default;
// Or use a custom scheduler
CacheDatabase.TaskpoolScheduler = new EventLoopScheduler();
Cache Inspection
// 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();
GetAllKeysSafe Methods
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
Cache Maintenance
// 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));
Mixed Object Storage
// 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
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.
Features
- 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
Installation
<PackageReference Include="Akavache.Drawing" Version="11.1.*" />
Dependencies
Akavache.Drawing requires:
Akavache.Core
- Core caching functionalitySplat.Drawing
- Cross-platform bitmap abstractions
Basic Usage
1. Initialize Drawing Support
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();
2. Load Images from Cache
// 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();
}
3. Load Images from URLs
// 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");
4. Save Images to Cache
// 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);
Advanced Features
Batch Image Operations
// 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));
Image Fallbacks
// 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);
Thumbnail Generation
// 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");
Image Size Detection
// 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();
}
Cache Management
// 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));
Complete Example: Photo Gallery App
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
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.
Features
- 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
Installation
<PackageReference Include="Akavache.Settings" Version="11.1.*" />
Basic Usage
1. Create a Settings Class
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
}
2. Initialize Settings Store
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}");
Advanced Configuration
Custom Settings Cache Path
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));
Multiple Settings Classes
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));
Encrypted 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";
Override Database Names
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
Complete Example
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;
Settings Lifecycle Management
Cleanup on Application Exit
// 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 Settings (Reset to Defaults)
// Delete a specific settings store
var builder = CacheDatabase.Builder;
await builder.DeleteSettingsStore<AppSettings>();
// Settings will be recreated with default values on next access
Check if Settings Exist
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");
}
Platform-Specific Notes
.NET MAUI
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 applicationsnet9.0-ios
- iOS applicationsnet9.0-maccatalyst
- Mac Catalyst applicationsnet9.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>
</PropertyGroup>
</Project>
WPF Applications
// 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());
}
}
iOS Specific
// 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);
}
Android Specific
// 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());
}
UWP Applications
// 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
.
Performance
Benchmarks
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.
Key Performance Metrics
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 |
V11 vs V10 Performance Comparison
- 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
Serializer Performance Comparison
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
Known Limitations
- 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
Serialization and Versioning Notes
- 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
Performance Reports
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
Reproducing the Benchmarks
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
Performance Tips
// 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
Best Practices
1. Initialization
// โ
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
2. Key Naming
// โ
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);
3. Error Handling
// โ
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"));
4. Cache Types Usage
// โ
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
5. Expiration
// โ
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
6. Shutdown
// โ
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();
}
7. Testing
// โ
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();
}
Troubleshooting
Common Issues
1. "No serializer has been registered"
// Fix: Register a suitable serializer during initialization
CacheDatabase.Initialize<SystemJsonSerializer>(/* ... */);
AppBuilder.CreateSplatBuilder()
.WithAkavache<SystemJsonSerializer>(/* ... */);
AppBuilder.CreateSplatBuilder()
.WithAkavacheCacheDatabase<SystemJsonSerializer>(/* ... */);
2. "CacheDatabase has not been initialized"
// Fix: Call Initialize before using cache
CacheDatabase.Initialize<SystemJsonSerializer>(builder => builder.WithApplicationName("MyApp").WithInMemoryDefaults());
var data = await CacheDatabase.UserAccount.GetObject<MyData>("key");
3. Data compatibility issues
// Fix: Use cross-compatible serializer or migration
CacheDatabase.Initialize<NewtonsoftBsonSerializer>(/* ... */); // Most compatible
4. SQLite errors on mobile
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:
<ItemGroup>
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.11" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SQLitePCLRaw.bundle_green" Version="2.1.11" />
</ItemGroup>
<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.
5. Linker removing types IL2104
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;
}
}
6. Provider not found errors
Ensure you have the appropriate SQLitePCLRaw bundle installed for your platform:
// For general use
.WithSqliteProvider()
// For encrypted databases
.WithEncryptedSqliteProvider()
7. GetOrFetchObject returns stale data after Invalidate (Fixed in V11.1.1+)
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.
UWP x64 Issues
Ensure your UWP project targets a specific platform (x86, x64, ARM) rather than "Any CPU".
Support and Contributing
- ๐ Documentation: https://github.com/reactiveui/Akavache
- ๐ Issues: GitHub Issues
- ๐ฌ Chat: ReactiveUI Slack
- ๐ฆ NuGet: Akavache Packages
Thanks
This project is tested with BrowserStack.
License
Akavache is licensed under the MIT License.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-android35.0 is compatible. net9.0-browser was computed. net9.0-ios was computed. net9.0-ios18.0 is compatible. net9.0-maccatalyst was computed. net9.0-maccatalyst18.0 is compatible. net9.0-macos was computed. net9.0-macos15.0 is compatible. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 9.0.8)
- Splat.Builder (>= 16.2.1)
- sqlite-net-pcl (>= 1.9.172)
- SQLitePCLRaw.bundle_green (>= 2.1.11)
- SQLitePCLRaw.lib.e_sqlite3 (>= 2.1.11)
- System.Reactive (>= 6.0.2)
-
net8.0
- Microsoft.Bcl.AsyncInterfaces (>= 9.0.8)
- Splat.Builder (>= 16.2.1)
- sqlite-net-pcl (>= 1.9.172)
- SQLitePCLRaw.bundle_green (>= 2.1.11)
- SQLitePCLRaw.lib.e_sqlite3 (>= 2.1.11)
- System.Reactive (>= 6.0.2)
-
net9.0
- Microsoft.Bcl.AsyncInterfaces (>= 9.0.8)
- Splat.Builder (>= 16.2.1)
- sqlite-net-pcl (>= 1.9.172)
- SQLitePCLRaw.bundle_green (>= 2.1.11)
- SQLitePCLRaw.lib.e_sqlite3 (>= 2.1.11)
- System.Reactive (>= 6.0.2)
-
net9.0-android35.0
- Microsoft.Bcl.AsyncInterfaces (>= 9.0.8)
- Splat.Builder (>= 16.2.1)
- sqlite-net-pcl (>= 1.9.172)
- SQLitePCLRaw.bundle_green (>= 2.1.11)
- System.Reactive (>= 6.0.2)
-
net9.0-ios18.0
- Microsoft.Bcl.AsyncInterfaces (>= 9.0.8)
- Splat.Builder (>= 16.2.1)
- sqlite-net-pcl (>= 1.9.172)
- SQLitePCLRaw.bundle_green (>= 2.1.11)
- System.Reactive (>= 6.0.2)
-
net9.0-maccatalyst18.0
- Microsoft.Bcl.AsyncInterfaces (>= 9.0.8)
- Splat.Builder (>= 16.2.1)
- sqlite-net-pcl (>= 1.9.172)
- SQLitePCLRaw.bundle_green (>= 2.1.11)
- SQLitePCLRaw.lib.e_sqlite3 (>= 2.1.11)
- System.Reactive (>= 6.0.2)
-
net9.0-macos15.0
- Microsoft.Bcl.AsyncInterfaces (>= 9.0.8)
- Splat.Builder (>= 16.2.1)
- sqlite-net-pcl (>= 1.9.172)
- SQLitePCLRaw.bundle_green (>= 2.1.11)
- SQLitePCLRaw.lib.e_sqlite3 (>= 2.1.11)
- System.Reactive (>= 6.0.2)
NuGet packages (61)
Showing the top 5 NuGet packages that depend on Akavache:
Package | Downloads |
---|---|
Akavache.Sqlite3
Package Description |
|
SheshaMobile.Core
Common application functionality and features to be shared across the framework |
|
SheshaMobile.Modules
The modules module contains common functionality shared across multiple modules |
|
SheshaMobile.Modules.UserProfile
A module for building apps with user functionality |
|
SheshaMobile.Modules.Facilities
The facilities module contains common functionality for browsing and viewing facilities |
GitHub repositories (15)
Showing the top 15 popular GitHub repositories that depend on Akavache:
Repository | Stars |
---|---|
CodeHubApp/CodeHub
CodeHub is an iOS application written using Xamarin
|
|
MoocDownloader/MoocDownloader
An MOOC downloader implemented by .NET. ไธๆ็ฑ .NET ๅฎ็ฐ็ MOOC ไธ่ฝฝๅจ.
|
|
reactiveui/Camelotia
Cross-platform sample .NET GUI for cloud file management.
|
|
reactiveui/ReactiveUI.Samples
This repository contains ReactiveUI samples.
|
|
nor0x/Dots
the 🙂 friendly .NET SDK manager
|
|
mmbot/mmbot
A C# port of Hubot
|
|
flagbug/Espera
Espera is a media player that plays your music, YouTube videos, SoundCloud songs and has a special "party mode".
|
|
Clancey/gMusic
This is a multi platform music player.
|
|
thedillonb/CodeBucket
CodeBucket is the best way to browse and maintain your Bitbucket repositories on any iPhone, iPod Touch, and iPad device!
|
|
Ombrelin/plex-rich-presence
A desktop app to enable discord rich presence for your Plex Media Server Activity
|
|
Respawnsive/Apizr
Refit based web api client management, but resilient (retry, connectivity, cache, auth, log, priority, etc...)
|
|
kentcb/WorkoutWotch
Repository for my video series on building an iOS app in .NET.
|
|
Titlehhhh/Minecraft-Holy-Client
A high-performance platform for running Minecraft stress-test bots written in C#.
|
|
thedillonb/RepoStumble
A mobile application for viewing GitHub repositories in a fashion similar to StumbleUpon
|
|
sthewissen/MVP
Unofficial app to help MVPs manage their community activities
|
Version | Downloads | Last Updated |
---|---|---|
11.3.3 | 47 | 9/6/2025 |
11.1.1 | 258 | 9/2/2025 |
11.0.1 | 594 | 8/24/2025 |
10.2.41 | 10,288 | 3/16/2025 |
10.1.6 | 116,450 | 9/16/2024 |
10.0.1 | 84,643 | 5/1/2024 |
9.1.20 | 281,186 | 6/29/2023 |
9.1.7 | 68,414 | 2/1/2023 |
9.0.1 | 236,769 | 6/25/2022 |
8.1.1 | 212,675 | 12/12/2021 |
7.3.47 | 14,376 | 11/26/2021 |
7.3.1 | 138,758 | 6/6/2021 |
7.2.1 | 250,924 | 1/22/2021 |
7.1.1 | 183,258 | 10/23/2020 |
6.10.20 | 4,266,314 | 6/12/2020 |
6.10.17 | 50,531 | 5/7/2020 |
6.10.11 | 14,445 | 4/23/2020 |
6.10.6 | 14,559 | 4/1/2020 |
6.10.4 | 48,840 | 2/5/2020 |
6.10.3 | 9,601 | 1/29/2020 |
6.10.2 | 6,466 | 1/27/2020 |
6.10.1 | 1,125 | 1/27/2020 |
6.9.10 | 43,755 | 11/4/2019 |
6.9.1 | 40,466 | 10/5/2019 |
6.8.1 | 17,287 | 9/6/2019 |
6.7.1 | 15,054 | 8/6/2019 |
6.6.1 | 1,340 | 8/6/2019 |
6.5.20 | 18,466 | 7/28/2019 |
6.5.9 | 32,528 | 6/7/2019 |
6.5.1 | 91,653 | 3/27/2019 |
6.4.1 | 7,387,108 | 3/3/2019 |
6.3.6 | 8,469 | 2/19/2019 |
6.3.2 | 19,026 | 2/1/2019 |
6.3.1 | 3,162 | 1/25/2019 |
6.2.3 | 20,386 | 12/27/2018 |
6.2.1 | 1,385 | 12/27/2018 |
6.1.3 | 1,899 | 12/24/2018 |
6.1.2 | 2,307 | 12/24/2018 |
6.1.1 | 2,007 | 12/23/2018 |
6.0.31 | 40,307 | 11/11/2018 |
6.0.30 | 41,798 | 10/17/2018 |
6.0.27 | 14,431 | 10/5/2018 |
6.0.20 | 56,308 | 9/5/2018 |
6.0.19-beta | 1,225 | 9/3/2018 |
6.0.17-beta | 2,162 | 8/24/2018 |
6.0.0-alpha0038 | 78,545 | 9/3/2017 |
5.0.0 | 502,942 | 11/4/2016 |
4.1.2 | 84,039 | 10/27/2015 |
4.1.1 | 20,809 | 3/26/2015 |
4.1.0 | 5,692 | 1/3/2015 |
4.0.4 | 3,646 | 11/6/2014 |
4.0.3.2 | 6,934 | 8/28/2014 |
4.0.3.1 | 1,910 | 8/28/2014 |
4.0.3 | 1,850 | 8/28/2014 |
4.0.2 | 1,891 | 8/25/2014 |
4.0.1 | 2,039 | 8/8/2014 |
4.0.0 | 2,051 | 8/3/2014 |
3.99.3-beta | 1,586 | 7/25/2014 |
3.99.2-beta | 1,453 | 7/17/2014 |
3.99.1-beta | 2,043 | 2/8/2014 |
3.2.0 | 4,445 | 12/11/2013 |
3.1.2 | 3,331 | 11/20/2013 |
3.1.1 | 15,020 | 10/12/2013 |
3.1.0 | 2,836 | 10/12/2013 |
3.0.2 | 7,710 | 8/9/2013 |
3.0.1 | 3,281 | 7/3/2013 |
3.0.0.20130620-alpha | 2,284 | 6/21/2013 |
3.0.0.20130531-alpha | 2,280 | 6/1/2013 |
3.0.0.20130519-alpha | 2,199 | 5/20/2013 |
3.0.0.20130513-alpha | 2,184 | 5/14/2013 |
2.6.12 | 3,022 | 4/1/2014 |
2.6.11 | 2,836 | 2/21/2014 |
2.6.10 | 2,815 | 1/14/2014 |
2.6.9 | 3,126 | 9/11/2013 |
2.6.8 | 3,194 | 8/22/2013 |
2.6.7 | 3,371 | 6/18/2013 |
2.6.6 | 3,952 | 5/2/2013 |
2.6.5 | 2,923 | 5/1/2013 |
2.6.4 | 3,067 | 4/2/2013 |
2.6.3 | 2,930 | 4/2/2013 |
2.6.2 | 3,068 | 3/13/2013 |
2.6.1 | 3,015 | 3/6/2013 |
2.6.0 | 3,203 | 3/5/2013 |
2.5.1 | 3,109 | 2/18/2013 |
2.5.0 | 3,099 | 2/11/2013 |
2.4.3 | 3,018 | 2/3/2013 |
2.4.1 | 2,692 | 1/14/2013 |
2.4.0 | 2,798 | 1/8/2013 |
2.3.1 | 2,778 | 1/8/2013 |
2.3.0 | 2,053 | 12/26/2012 |
2.2.2 | 1,984 | 12/20/2012 |
2.2.1 | 2,020 | 12/20/2012 |
2.2.0 | 1,926 | 12/20/2012 |
2.0.0 | 2,248 | 11/3/2012 |
1.0.1 | 2,489 | 4/28/2012 |
1.0.0 | 2,355 | 4/28/2012 |