A modern, SOLID-compliant application data storage library for .NET that provides type-safe persistence with full dependency injection support.
- 🏗️ SOLID Architecture: Clean separation of concerns with dependency injection
- 🎯 Type Safety: Strongly-typed paths using
ktsu.Semantics - 🔧 Dependency Injection: Full DI support with standard .NET patterns
- 🧪 Testable: Easy mocking and isolated unit testing
- 📁 Backup & Recovery: Automatic backup and corruption recovery
- ⚡ Performance: Debounced saves and efficient file operations
- 🔒 Thread Safe: Safe for concurrent access
- 📦 Zero Dependencies: Minimal external dependencies
dotnet add package ktsu.AppData- Configure services (Program.cs):
using ktsu.AppData.Configuration;
var services = new ServiceCollection();
services.AddAppData();
services.AddTransient<IMyService, MyService>();
using var serviceProvider = services.BuildServiceProvider();- Create your data model:
using ktsu.AppData;
public class UserSettings : AppData<UserSettings>
{
public string Theme { get; set; } = "Light";
public string Language { get; set; } = "English";
public Dictionary<string, string> Preferences { get; set; } = new();
}- Use in your services:
using ktsu.AppData.Interfaces;
public class MyService
{
private readonly IAppDataRepository<UserSettings> _repository;
public MyService(IAppDataRepository<UserSettings> repository)
{
_repository = repository;
}
public void SaveUserPreferences(string theme, string language)
{
var settings = new UserSettings
{
Theme = theme,
Language = language
};
settings.Save(_repository);
}
public UserSettings LoadUserPreferences()
{
return _repository.LoadOrCreate();
}
}// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add AppData services
builder.Services.AddAppData(options =>
{
// Custom JSON options
options.JsonSerializerOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
});
// Add your services
builder.Services.AddScoped<IUserService, UserService>();
var app = builder.Build();public class DatabaseConfig : AppData<DatabaseConfig>
{
public string ConnectionString { get; set; } = "";
public int TimeoutSeconds { get; set; } = 30;
// Save to custom subdirectory
protected override RelativeDirectoryPath? Subdirectory =>
"database".As<RelativeDirectoryPath>();
// Use custom filename
protected override FileName? FileNameOverride =>
"db_config.json".As<FileName>();
}public class RealTimeService
{
private readonly IAppDataRepository<AppState> _repository;
private readonly AppState _state = new();
public RealTimeService(IAppDataRepository<AppState> repository)
{
_repository = repository;
}
public void UpdateState(string key, string value)
{
_state.Data[key] = value;
_state.QueueSave(); // Queues save, doesn't write immediately
}
public async Task FlushChanges()
{
_state.SaveIfRequired(_repository); // Only saves if debounce time elapsed
}
}The library provides excellent testing support with mock file systems:
[Test]
public async Task UserService_SavesSettings_Successfully()
{
// Arrange
var services = new ServiceCollection();
services.AddAppDataForTesting(() => new MockFileSystem());
services.AddTransient<IUserService, UserService>();
using var serviceProvider = services.BuildServiceProvider();
var userService = serviceProvider.GetRequiredService<IUserService>();
// Act
userService.SaveUserPreferences("Dark", "Spanish");
// Assert
var settings = userService.LoadUserPreferences();
Assert.AreEqual("Dark", settings.Theme);
Assert.AreEqual("Spanish", settings.Language);
}services.AddAppData();services.AddAppData(options =>
{
options.JsonSerializerOptions = new JsonSerializerOptions
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
});services.AddAppData(options =>
{
options.FileSystemFactory = _ => new MyCustomFileSystem();
});services.AddAppData();
// Replace with custom implementations
services.Replace(ServiceDescriptor.Singleton<IAppDataSerializer, XmlSerializer>());
services.Replace(ServiceDescriptor.Singleton<IAppDataPathProvider, CustomPathProvider>());The library follows SOLID principles with a clean, dependency-injection-based architecture:
graph TD
A[AppData<T><br/>Data Model] --> B[IAppDataRepository<T><br/>Operations]
B --> C[IAppDataFileManager<br/>File Operations]
B --> D[IAppDataSerializer<br/>JSON Serialization]
B --> E[IAppDataPathProvider<br/>Path Management]
C --> E
F[Your Service] --> B
G[DI Container] --> B
G --> C
G --> D
G --> E
IAppDataRepository<T>: High-level data operations (Load, Save)IAppDataFileManager: File I/O with backup/recoveryIAppDataSerializer: Data serialization (JSON by default)IAppDataPathProvider: Type-safe path management
Data is stored in the user's application data directory:
Windows: %APPDATA%\{ApplicationName}\
macOS: ~/Library/Application Support/{ApplicationName}/
Linux: ~/.config/{ApplicationName}/
Files are saved with automatic backup and recovery:
- Primary file:
user_settings.json - Backup file:
user_settings.json.bk(temporary during writes) - Recovery: Automatic restoration from backup if primary file is corrupted
Old (v1.x):
public class Settings : AppData<Settings>
{
public string Theme { get; set; }
}
// Static usage
var settings = Settings.Get();
settings.Theme = "Dark";
settings.Save();New (v2.x):
public class Settings : AppData<Settings>
{
public string Theme { get; set; }
}
// Dependency injection
public class MyService
{
private readonly IAppDataRepository<Settings> _repository;
public MyService(IAppDataRepository<Settings> repository)
{
_repository = repository;
}
public void UpdateTheme(string theme)
{
var settings = _repository.LoadOrCreate();
settings.Theme = theme;
settings.Save(_repository);
}
}Always inject IAppDataRepository<T> rather than using static methods:
✅ Good:
public MyService(IAppDataRepository<Settings> repository)
{
_repository = repository;
}❌ Avoid:
var repository = AppData.GetRepository<Settings>(); // Static accessSave queued changes before disposal:
using var settings = new Settings();
settings.QueueSave();
settings.SaveIfRequired(repository); // Save before disposalOverride paths for logical grouping:
public class DatabaseSettings : AppData<DatabaseSettings>
{
protected override RelativeDirectoryPath? Subdirectory =>
"database".As<RelativeDirectoryPath>();
}
public class UiSettings : AppData<UiSettings>
{
protected override RelativeDirectoryPath? Subdirectory =>
"ui".As<RelativeDirectoryPath>();
}Always use AddAppDataForTesting() in unit tests:
services.AddAppDataForTesting(() => new MockFileSystem());Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
This project is licensed under the MIT License - see the LICENSE.md file for details.
ktsu.Semantics- Type-safe semantic typesktsu.CaseConverter- String case conversion utilitiesktsu.ToStringJsonConverter- Custom JSON converters