Automatically create strongly typed C# settings objects from AppSettings.json. Uses Source Generators.
Includes a simple deserialization helper for when you are using Dependency Injection, or not.
- NotNot.AppSettings
- Table of Contents
- Getting Started
- How it works
- Example
- State Management with AppSettingsManager
- Storage Providers
- Troubleshooting / Tips
- How to access the
AppSettingsclass from external code? - How to extend the generated
AppSettingsclass? - Some settings not being loaded (value is
NULL). Or: MyappSettings.Development.jsonfile is not loaded - Intellisense not working for
AppSettingsclass - Why are some of my nodes typed as
object? - Tip: Backup generated code in your git repository
- How to access the
- Contribute
- Acknowledgments
- License: MPL-2.0
- Notable Changes
- Add an
appsettings.jsonfile to your project (make sure it's copied to the output). - Install this nuget package
NotNot.AppSettings. - Build your project
- Use the generated
AppSettingsclass in your code. (See the example section below).
During your project's build process, NotNot.AppSettings will parse the appsettings*.json in your project's root folder. These files are all merged into a single schema. Using source-generators it then creates a set of csharp classes that matches each node in the json hierarchy.
After building your project, an AppSettings class contains the strongly-typed definitions,
and an AppSettingsBinder helper/loader util will be found under the {YourProjectRootNamespace}.AppSettingsGen namespace.
appsettings.json
{
"Hello": {
"World": "Hello back at you!"
}
}Program.cs
using ExampleApp.AppSettingsGen;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace ExampleApp;
public class Program
{
public static async Task Main(string[] args)
{
{
Console.WriteLine("NON-DI EXAMPLE");
var appSettings = ExampleApp.AppSettingsGen.AppSettingsBinder.LoadDirect();
Console.WriteLine(appSettings.Hello.World);
}
{
Console.WriteLine("DI EXAMPLE");
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<IAppSettingsBinder, AppSettingsBinder>();
var app = builder.Build();
var appSettings = app.Services.GetRequiredService<IAppSettingsBinder>().AppSettings;
Console.WriteLine(appSettings.Hello.World);
}
}
}See the ./NotNot.AppSettings.Example folder in the repository for a fully buildable version of this example.
There's now an IConfiguration extension method to make usage even easier:
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var appSettings = builder.Configuration._AppSettings();
Console.WriteLine($"appSettings.AllowedHosts={appSettings.AllowedHosts}");For applications requiring runtime settings persistence, auto-save, and reset to defaults, use AppSettingsManager<T> from the NotNot.Bcl package:
using NotNot.AppSettingsHelper;
// Load settings
var manager = new AppSettingsManager<AppSettings>();
await manager.LoadAsync(default, "appsettings.json", "appsettings.Development.json");
// Enable auto-save (changes saved after 500ms debounce)
manager.EnableAutoSave();
// Modify settings - automatically persisted to appsettings.user.json
manager.Settings.Window.X = 100;
manager.Settings.Window.Y = 200;
// Manual operations
await manager.SaveAsync(); // Immediate save
await manager.ReloadAsync(); // Reload from disk
await manager.ResetToDefaultsAsync(); // Delete user file, reload defaults
// Cleanup
await manager.DisposeAsync();Note: Requires reference to NotNot.Bcl package (Install-Package NotNot.Bcl).
| Method | Save-Capable | Use Case |
|---|---|---|
LoadAsync(ct, params paths) |
Yes | File-based settings with layered merge |
LoadAsync(streams, ct) |
Yes | Stream-based settings |
LoadFromConfigurationAsync(config, basePath, ct) |
Yes | IConfiguration with save support |
LoadFromConfiguration(config) |
No | Read-only mode |
// Custom debounce interval
manager.EnableAutoSave(TimeSpan.FromSeconds(2));
// Error handling
manager.OnAutoSaveError = ex => Console.WriteLine($"Auto-save failed: {ex}");
// Disable auto-save (optionally save pending changes)
await manager.DisableAutoSaveAsync(saveNow: true);Changes are persisted to a separate user file (default: appsettings.user.json). Only changed values are stored (diff-based).
manager.UserSettingsPath = "config/user-settings.json";| Method | Behavior |
|---|---|
Clear() |
Reset to base settings in memory (triggers auto-save) |
ResetToDefaultsAsync() |
Delete user file, reload from base files |
ReloadAsync() |
Discard memory changes, reload from disk |
- Array element mutations: In-place changes like
items[0] = xare NOT tracked. Reassign the entire array instead. - Stream-based load: Cannot distinguish base from user layers for reset operations.
For platforms without file system access (Blazor, sandboxed environments), use storage providers:
The ISettingsStorageProvider interface abstracts settings persistence:
public interface ISettingsStorageProvider
{
ValueTask<string?> ReadAsync(CancellationToken ct = default);
ValueTask WriteAsync(string json, CancellationToken ct = default);
ValueTask DeleteAsync(CancellationToken ct = default);
ValueTask<bool> ExistsAsync(CancellationToken ct = default);
}Built-in file system storage provider for desktop/server apps:
using NotNot.AppSettingsHelper;
// Create storage provider for a specific file
var storage = new FileStorageProvider("/path/to/settings.json");
// Load settings using the storage provider
var manager = new AppSettingsManager<AppSettings>();
await manager.LoadFromStorageAsync(storage);
// Enable auto-save - changes persisted to the storage provider
manager.EnableAutoSave();
// Modify settings
manager.Settings.Theme = "dark";
// Cleanup
await manager.DisposeAsync();Features:
- Automatic directory creation on first write
- Graceful handling of locked/inaccessible files
- Thread-safe for debounced auto-save
For Blazor apps, use LocalStorageStorageProvider from NotNot.BlazorComponents:
// In Blazor component or service
@inject IJSRuntime JSRuntime
var storage = new LocalStorageStorageProvider(JSRuntime, "app-settings");
var manager = new AppSettingsManager<AppSettings>();
await manager.LoadFromStorageAsync(storage);
manager.EnableAutoSave();Implement ISettingsStorageProvider for custom backends (cloud, IndexedDB, etc.):
public class IndexedDbStorageProvider : ISettingsStorageProvider
{
private readonly IJSRuntime _jsRuntime;
private readonly string _storeName;
public IndexedDbStorageProvider(IJSRuntime jsRuntime, string storeName)
{
_jsRuntime = jsRuntime;
_storeName = storeName;
}
public async ValueTask<string?> ReadAsync(CancellationToken ct = default)
{
try
{
return await _jsRuntime.InvokeAsync<string?>("indexedDbGet", ct, _storeName);
}
catch
{
return null; // Use defaults
}
}
public async ValueTask WriteAsync(string json, CancellationToken ct = default)
{
await _jsRuntime.InvokeVoidAsync("indexedDbSet", ct, _storeName, json);
}
public async ValueTask DeleteAsync(CancellationToken ct = default)
{
await _jsRuntime.InvokeVoidAsync("indexedDbDelete", ct, _storeName);
}
public async ValueTask<bool> ExistsAsync(CancellationToken ct = default)
{
return await _jsRuntime.InvokeAsync<bool>("indexedDbExists", ct, _storeName);
}
}To prevent namespace collisions with other projects, the generated classes are internal by default.
If you need to access it from another project, the best solution is to make a wrapper class in the project that uses the generated code.
Alternatively, you can make the generated code public:
- v
2.0.0and later: you can add the following to your.csprojfile:
<PropertyGroup>
<NotNot_AppSettings_GenPublic>true</NotNot_AppSettings_GenPublic>
</PropertyGroup>- v
1.xand earlier: TheAppSettingsclass ispublicby default.
You can extend any/all of the generated code by creating a partial class in the same namespace.
Some settings not being loaded (value is NULL). Or: My appSettings.Development.json file is not loaded
Ensure the proper environment variable is set. For example, The appSettings.Development.json file is only loaded when the ASPNETCORE_ENVIRONMENT
or DOTNET_ENVIORNMENT environment variable is set to Development.
A strongly-typed AppSettings (and sub-classes) is recreated every time you build your project.
This may confuse your IDE and you might need to restart it to get intellisense working again.
Under some circumstances, the type of a node's value in appsettings.json would be ambiguous, so object is used:
- If the value is
nullorundefined - If the value is a POJO/Array/primitive in one appsettings file, and a different one of those three in another.
Add this to your .csproj to have the code output to ./Generated and have it be ignored by your project.
This way you can check it into source control and have a backup of the generated code in case you need to stop using this package.
<!--output the source generator build files-->
<Target Name="DeleteFolder" BeforeTargets="PreBuildEvent">
<RemoveDir Directories="$(CompilerGeneratedFilesOutputPath)" />
</Target>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<!--Exclude the output of source generators from the compilation-->
<Compile Remove="$(CompilerGeneratedFilesOutputPath)/**" />
</ItemGroup>- If you find value from this project, consider sponsoring.
- Add
OutputItemType="Analyzer" ReferenceOutputAssembly="false"to the<ProjectReference/> - IMPORTANT: When using Project References, you must manually import the targets file to expose MSBuild properties to the source generator:
<!-- Import NotNot.AppSettings targets to expose MSBuild properties to source generator -->
<Import Project="path/to/NotNot.AppSettings.targets" />Without this import, properties like <NotNot_AppSettings_GenPublic>true</NotNot_AppSettings_GenPublic> will be ignored.
- beware when attempting to update nuget packages, it will likely break the Source Generator. Default to just leaving them as is, unless you want to spend time troubleshooting sourcegen thrown exceptions.
- current version is set via
MinVer, which matches the repo git tags. - read the repo's
Contrib/folder for more info.
- This project was inspired by https://github.com/FrodeHus/AppSettingsSourceGenerator which unfortunately did not match my needs in fundamental ways.
A summary from TldrLegal:
MPL is a copyleft license that is easy to comply with. You must make the source code for any of your changes available under MPL, but you can combine the MPL software with proprietary code, as long as you keep the MPL code in separate files. Version 2.0 is, by default, compatible with LGPL and GPL version 2 or greater. You can distribute binaries under a proprietary license, as long as you make the source available under MPL.
In brief: You can basically use this project however you want, but all changes to it must be open sourced.
3.0.0:- NEW:
AppSettingsManager<T>for runtime settings persistence, auto-save, and reset operations - NEW:
JsonSettingsUtilsfor diff-based save (only changed values persisted) - NEW:
ISettingsChangeAwareinterface for change tracking (source-generated) - Breaking: Generated properties now use backing fields for change detection
- Runtime features require
NotNot.Bclpackage reference
- NEW:
2.0.3:- New IConfiguration extension method to make usage easier
2.0.2:- Generated code files now use the
.g.cssuffix (a7dc012) - Added an extension method for faster Dependency Injection acquisition (7fd7f1f)
- Improved Linux compatibility for finding
appsettings.jsonfiles (a29270f)
- Generated code files now use the
2.0.0: generated code is nowinternalby default. Make it public by adding<NotNot_AppSettings_GenPublic>true</NotNot_AppSettings_GenPublic>to your.csproj1.2.1: improve doc for missing appSettings.json, handle projects with blank default namespace. move to new repository.1.1.1: make the nuget package<PrivateAsset>so only the project that directly references it uses it.- (needed for example: test projects)
1.0.0: polish and readme tweaks. Put a fork in it, it's done!0.12.0: change appsettings read logic to use "AdditionalFiles" workflow instead of File.IO0.10.0: Initial Release.