# **Akavache.Settings** Welcome to the `Akavache.Settings` guide\! This library provides a specialized, type-safe database for managing application settings. It creates persistent settings that are stored securely, making application updates and reinstalls seamless, as your users' configurations will survive the process. It’s built on the powerful and asynchronous Akavache key-value store, offering a simple, object-oriented way to handle everything from user preferences to complex configuration objects. ### **Features** * **Type-Safe Settings**: Define settings as strongly-typed properties with default values. No more magic strings\! * **Automatic Persistence**: Settings are automatically saved to a SQLite database when changed. * **Update Friendly**: Settings are stored in a way that allows them to persist even when your application is updated or reinstalled. * **Encrypted Storage**: Built-in support for password-protecting sensitive settings like API keys or tokens. * **Multiple Settings Classes**: Easily organize your settings by grouping them into separate classes (e.g., `UserSettings`, `NetworkSettings`). * **DI Friendly**: Designed from the ground up to integrate with modern dependency injection patterns. ----- ## **Tutorial: Mastering Application Settings** This tutorial will guide you through setting up and using `Akavache.Settings` in a modern .NET application. We'll cover everything from creating your first settings class to advanced topics like encryption and lifecycle management. ### **A Note on Modern Initialization** If you've used older versions of Akavache, you might be familiar with setting a static `BlobCache.ApplicationName`. Akavache has since moved to a modern, **fluent builder pattern** for initialization. This change was made for several important reasons: * **Testability**: The new approach makes it easy to use an in-memory cache during unit tests, isolating tests from the file system. * **Clarity & Control**: Configuration is now explicit and declarative. You can see exactly how the cache is configured in one place. * **Dependency Injection (DI)**: It integrates perfectly with DI containers like Splat, which is the recommended approach for all modern applications. While the core static class `CacheDatabase` is the engine that powers this, we'll be using the `Splat` integration builder (`AppBuilder.CreateSplatBuilder()`) in this guide, as it represents the best practice for building maintainable applications. ### **Step 1: Installation** First, add the `Akavache.Settings` package to your project. You'll also need a serializer like `Akavache.SystemTextJson` and a backend like `Akavache.Sqlite3`. ```xml ``` ### **Step 2: Create Your First Settings Class** Define a class that inherits from `SettingsBase`. This class will hold your application's settings as properties. ```csharp using Akavache.Settings; public class AppSettings : SettingsBase { // The constructor must call the base constructor with a unique name for this // settings store. This name becomes the database filename. public AppSettings() : base(nameof(AppSettings)) { } // A boolean setting. // get => GetOrCreate(true): If a value exists in the database, it's returned. // If not, the default value 'true' is returned and saved for next time. public bool EnableNotifications { get => GetOrCreate(true); set => SetOrCreate(value); // Saves the new value to the database. } // A string setting with a default value. public string UserName { get => GetOrCreate("DefaultUser"); set => SetOrCreate(value); } // An enum setting. public LogLevel LoggingLevel { get => GetOrCreate(LogLevel.Information); set => SetOrCreate(value); } } public enum LogLevel { Debug, Information, Warning, Error } ``` The magic happens in the **`GetOrCreate()`** and **`SetOrCreate()`** methods. They handle all the database interaction, so you can work with your settings as simple C\# properties. ### **Step 3: Initialize the Settings Store** At your application's startup (e.g., in `MauiProgram.cs` or `App.xaml.cs`), initialize Akavache and register your new settings class. ```csharp using Akavache.Core; using Akavache.SystemTextJson; using Akavache.Settings; // This variable will be populated with our live settings object after initialization. var appSettings = default(AppSettings); AppBuilder.CreateSplatBuilder().WithAkavache(builder => builder .WithApplicationName("MyApp") .WithSerializer(new SystemJsonSerializer()) .WithSqliteProvider() // This is the key method for settings. It creates an instance of // AppSettings and returns it in the callback. .WithSettingsStore(settings => appSettings = settings)); // Now the 'appSettings' variable can be used anywhere in your app. // For DI-heavy apps, you would typically register it as a singleton. ``` ### **Step 4: Use Your Settings** Once initialized, you can use the `appSettings` object to read and write settings. Changes are saved to the database automatically. ```csharp // Read a setting (will return the default "DefaultUser" on first run) Console.WriteLine($"Username: {appSettings.UserName}"); // Write a setting appSettings.UserName = "John Doe"; appSettings.EnableNotifications = false; Console.WriteLine($"New Username: {appSettings.UserName}"); // Prints "John Doe" // The next time the app starts, appSettings.UserName will be "John Doe". ``` ----- ## **Advanced Scenarios** `Akavache.Settings` is flexible enough to handle more complex requirements with ease. ### **Multiple Settings Classes** You can organize your settings by creating multiple classes. For example, you could have `UserSettings` and `NetworkSettings`. Simply register each one in the builder. ```csharp 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); } } // In your initialization: var userSettings = default(UserSettings); var networkSettings = default(NetworkSettings); AppBuilder.CreateSplatBuilder() .WithAkavache(builder => builder.WithApplicationName("MyApp") .WithSqliteProvider() .WithSettingsStore(s => userSettings = s) .WithSettingsStore(s => networkSettings = s)); ``` This will create two separate database files, `UserSettings.db` and `NetworkSettings.db`, keeping your configurations neatly separated. ### **Encrypted Settings** For sensitive data like API keys, you should use an encrypted store. The setup is nearly identical, but you use the **`WithEncryptedSqliteProvider`** and **`WithSecureSettingsStore`** methods and provide a password. ```csharp public class SecureSettings : SettingsBase { public SecureSettings() : base(nameof(SecureSettings)) { } public string ApiKey { get => GetOrCreate(string.Empty); set => SetOrCreate(value); } } var secureSettings = default(SecureSettings); AppBuilder.CreateSplatBuilder() .WithAkavache(builder => builder.WithApplicationName("MyApp") .WithEncryptedSqliteProvider() .WithSecureSettingsStore("my-super-secret-password", settings => secureSettings = settings)); // Use it just like a normal settings object. secureSettings.ApiKey = "sk-1234567890abcdef"; ``` ### **Customizing the Storage Path** By default, settings are stored in a standard application data location. You can override this using **`WithSettingsCachePath`**. ```csharp AppBuilder.CreateSplatBuilder() .WithAkavache(builder => builder.WithApplicationName("MyApp") .WithSqliteProvider() .WithSettingsCachePath(@"C:\MyApp\Settings") // Custom path .WithSettingsStore(settings => appSettings = settings)); ``` ### **Override Database Names** You can specify a custom database file name for a settings store. This is useful if you need to manage multiple instances of the same settings class, perhaps for different users or documents. To do this, simply provide a second argument to the `WithSettingsStore` method. ```csharp var appSettings = default(AppSettings); AppBuilder.CreateSplatBuilder() .WithAkavache(builder => builder.WithApplicationName("MyApp") .WithSqliteProvider() .WithSettingsStore( settings => appSettings = settings, "CustomAppConfig")); // This will create "CustomAppConfig.db" ``` ----- ## **Working with Complex & Custom Types** Akavache.Settings isn't limited to simple types. Thanks to its serializer, it can handle collections, nullable types, and even your own custom objects out of the box. Here's a comprehensive example showing a variety of data types in action: ```csharp public class ComprehensiveSettings : SettingsBase { public ComprehensiveSettings() : base(nameof(ComprehensiveSettings)) { } // Basic types with defaults public bool IsFeatureEnabled { get => GetOrCreate(true); set => SetOrCreate(value); } public int RetryCount { get => GetOrCreate(5); set => SetOrCreate(value); } public double ScaleFactor { get => GetOrCreate(1.25); set => SetOrCreate(value); } public string LastUser { get => GetOrCreate("Guest"); set => SetOrCreate(value); } // Nullable types public string? LastKnownLocation { get => GetOrCreate(null); set => SetOrCreate(value); } // Complex types (automatically serialized to JSON) public List FavoriteCities { get => GetOrCreate(new List { "New York", "London" }); set => SetOrCreate(value); } public Dictionary UserScores { get => GetOrCreate(new Dictionary { ["Player1"] = 100, ["Player2"] = 250 }); set => SetOrCreate(value); } // Custom objects public WindowPosition LastWindowPosition { get => GetOrCreate(new WindowPosition { X = 100, Y = 100, Width = 800, Height = 600 }); set => SetOrCreate(value); } } // Your custom class - no special attributes needed! 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(builder => builder.WithApplicationName("MyApp") .WithSqliteProvider() .WithSettingsStore(s => settings = s)); // Now you can manipulate the complex properties directly settings.FavoriteCities.Add("Tokyo"); settings.UserScores["Player3"] = 500; settings.LastWindowPosition = new WindowPosition { X = 200, Y = 150, Width = 1024, Height = 768 }; ``` ----- ## **Lifecycle Management** Properly managing the lifecycle of your settings is crucial for a robust application. ### **Resetting Settings to Defaults** Implementing a "Reset Settings" feature is simple. The **`DeleteSettingsStore`** method removes the underlying database file. The next time the settings class is accessed, it will be recreated with its default values. ```csharp // In a button click handler or command var builder = CacheDatabase.Builder; await builder.DeleteSettingsStore(); // You would typically inform the user that a restart is required. ``` ### **Proper Application Shutdown** To prevent data loss, it's important to flush any pending writes to the disk when your application closes. ```csharp // In your application shutdown logic (e.g., OnExit) public async Task OnApplicationExit() { var builder = CacheDatabase.Builder; // Dispose all your settings stores await builder.DisposeSettingsStore(); await builder.DisposeSettingsStore(); // And finally, shut down Akavache itself await CacheDatabase.Shutdown(); } ``` ### **Check if Settings Exist** Sometimes, you need to know if settings have been created before. This is useful for onboarding experiences or migrations. The `GetSettingsStore` method (without a callback) allows you to do this. It will return `null` if the store has not been created yet. ```csharp // After the main Akavache initialization... var builder = CacheDatabase.Builder; var existingSettings = builder.GetSettingsStore(); if (existingSettings != null) { Console.WriteLine("Settings already exist. Welcome back!"); } else { Console.WriteLine("This is the first run. Creating default settings."); } ``` ----- ## **Common Patterns** Here are some common patterns that can make your settings classes even more powerful. ### **Validation in Setters** You can add validation logic directly into the property setter to ensure that only valid data is saved. ```csharp public class ValidatedSettings : SettingsBase { public ValidatedSettings() : base(nameof(ValidatedSettings)) { } public int MaxConnections { get => GetOrCreate(10); set { if (value < 1 || value > 100) { throw new ArgumentOutOfRangeException(nameof(value), "MaxConnections must be between 1 and 100."); } SetOrCreate(value); } } } ``` ### **Settings Change Notifications** If you are using your settings class with a UI framework like WPF, MAUI, or Avalonia, you can implement `INotifyPropertyChanged` to have the UI update automatically when a setting changes. ```csharp using System.ComponentModel; using Akavache.Settings; public class NotifyingSettings : SettingsBase, INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; public NotifyingSettings() : base(nameof(NotifyingSettings)) { } public string Theme { get => GetOrCreate("Light"); set { // SetOrCreate returns 'true' if the value was changed if (SetOrCreate(value)) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Theme))); } } } } ``` ----- ## ** Best Practices** ### **1. Use Meaningful Setting Names** Descriptive names make your code easier to understand and maintain. ```csharp // ✅ Good - Clear and descriptive public bool EnablePushNotifications { get; set; } public string DefaultLanguageCode { get; set; } // ❌ Avoid - Ambiguous and unclear public bool Flag1 { get; set; } public string Str { get; set; } ``` ### **2. Provide Sensible Defaults** Every setting should have a reasonable default value in `GetOrCreate()` to ensure your app works correctly on the first run. ```csharp // ✅ Good - A sensible default that works out-of-the-box public int NetworkTimeoutSeconds { get => GetOrCreate(30); // 30 seconds is a reasonable starting point set => SetOrCreate(value); } // ❌ Avoid - A default that could cause issues public int TimeoutMs { get => GetOrCreate(0); // A 0ms timeout is likely an error set => SetOrCreate(value); } ``` ### **3. Group Related Settings** Organize settings into logical classes to improve separation of concerns. ```csharp // ✅ Good - Related settings are grouped together public class NetworkSettings : SettingsBase { public int TimeoutSeconds { get; set; } public int MaxRetries { get; set; } } // ✅ Good - UI settings are in their own class public class UISettings : SettingsBase { public string Theme { get; set; } public double FontSize { get; set; } } ``` ### **4. Use Encryption for Sensitive Data** Always use an encrypted store for any data that should not be stored in plain text. ```csharp // ✅ Good - Encrypt sensitive data like API keys public class SecuritySettings : SettingsBase { /* ... */ } // In initialization: builder.WithEncryptedSqliteProvider() .WithSecureSettingsStore("password", ...); ``` ## **Troubleshooting** If you run into issues, check these common solutions. * **Settings Not Persisting?** 1. **Check Initialization**: Make sure `WithSettingsStore()` is being called at startup. 2. **Verify Write Access**: Ensure your application has permission to write to its data directory. 3. **Call `SetOrCreate`**: A value is only saved when the `set` accessor is called. Reading a default value with `GetOrCreate` persists it, but subsequent changes require calling the setter. * **Performance Issues?** 1. **Avoid Overly Complex Objects**: While custom objects work, very large and deeply nested ones will be slower to serialize and deserialize. 2. **Batch Changes**: Settings are written to disk on every `SetOrCreate` call. If you need to change multiple settings at once, consider doing so in a single method to avoid frequent, small writes. * **Encryption Problems?** 1. **Store Passwords Securely**: Be sure to store your encryption password in a secure location, such as the platform's keychain or secure storage, rather than hardcoding it. 2. **Password Changes**: Changing the encryption password will make the old database file unreadable. This requires a migration path where you open the store with the old password, read the values, and save them to a new store with the new password. ----- ## **Conclusion & Best Practices** You've now seen how `Akavache.Settings` provides a powerful, type-safe, and modern way to manage configuration. By following these best practices, you can build more robust and maintainable software.