Application data storage library for .NET that provides type-safe persistence with dependency injection support. Features automatic backup and recovery, debounced saves, mock file system support for testing, and cross-platform storage using the user's app data directory.
$ dotnet add package ktsu.AppDataA modern, SOLID-compliant application data storage library for .NET that provides type-safe persistence with full dependency injection support.
ktsu.Semanticsdotnet add package ktsu.AppData
using ktsu.AppData.Configuration;
var services = new ServiceCollection();
services.AddAppData();
services.AddTransient<IMyService, MyService>();
using var serviceProvider = services.BuildServiceProvider();
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();
}
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 managementData 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:
user_settings.jsonuser_settings.json.bk (temporary during writes)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 access
Save queued changes before disposal:
using var settings = new Settings();
settings.QueueSave();
settings.SaveIfRequired(repository); // Save before disposal
Override 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