Runtime configuration editor for ASP.NET Core. View and modify IOptions<T> values at runtime through a built-in web UI without restarting the application, and manage feature flags the same way.
A live demo is available at kododo.dev/configway/demo.
dotnet add package Kododo.ConfigWay
dotnet add package Kododo.ConfigWay.UI
dotnet add package Kododo.ConfigWay.PostgreSQL # or Kododo.ConfigWay.SqlServer — optional, for persistencebuilder.AddConfigWay(x =>
{
x.AddOptions<EmailOptions>();
x.AddOptions<AppOptions>();
x.AddUiEditor();
x.UsePostgreSql(builder.Configuration.GetConnectionString("DefaultConnection")!);
// or: x.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")!);
});
app.UseConfigWay(); // mounts UI at /config// Section name inferred from type — "Options" suffix stripped
x.AddOptions<EmailOptions>(); // -> "Email"
// Override explicitly
x.AddOptions<EmailOptions>("Mail");Nested types are supported and rendered as collapsible subsections in the UI.
ConfigWay maps C# property types to dedicated UI controls automatically:
| C# type | UI control |
|---|---|
string |
Text input |
bool |
Toggle switch |
int, long, double, decimal, ... |
Numeric input |
enum |
Dropdown select |
T[], List<T>, IList<T>, ... |
Collapsible array editor |
Nullable variants (bool?, int?, etc.) are handled the same way.
Collection properties (T[], List<T>, IList<T>, IEnumerable<T>, IReadOnlyList<T>, ICollection<T>, IReadOnlyCollection<T>) are rendered as a collapsible array editor with add and remove buttons.
Simple arrays with scalar element types (string[], int[], Severity[], ...) show one input field per item:
public class WebhooksOptions
{
[Display(Name = "Allowed Origins")]
public string[] AllowedOrigins { get; set; } = [];
}Complex arrays with class element types show a full sub-form per item, supporting nested objects and all scalar field types:
public class WebhookEndpoint
{
public string Url { get; set; } = string.Empty;
public string Secret { get; set; } = string.Empty;
public WebhookEvent Event { get; set; }
}
public class WebhooksOptions
{
[Display(Name = "Endpoints")]
public WebhookEndpoint[] Endpoints { get; set; } = [];
}Items that come from lower configuration layers (appsettings.json, environment variables) are marked as non-deletable. They can be edited but not removed, because deleting them from the ConfigWay store would not suppress the underlying value.
Every field, section and array exposes a reset button that appears when the current value differs from the value in the underlying configuration layers (appsettings.json, environment variables, etc.). Resetting removes the ConfigWay-stored override so the original value from those lower layers takes effect again, without restarting the application.
| Scope | Behaviour |
|---|---|
| Field | Removes the single key from the ConfigWay store. The reset button is shown only when the field differs from its base-config value. |
| Section | Resets all fields and arrays inside the section recursively. Items added via ConfigWay are removed; non-deletable items are reset to their base-config values. |
| Array | Items added via ConfigWay are removed. Non-deletable items (those that exist in lower config layers) have their values reset to the base-config value. |
Pending resets are batched with any other edits and applied together when Save is pressed. Pressing Discard also discards pending resets.
Mark any string property with [DataType(DataType.Password)] from System.ComponentModel.DataAnnotations to treat it as a sensitive field:
using System.ComponentModel.DataAnnotations;
public class SmtpCredentials
{
public string Username { get; set; } = string.Empty;
[DataType(DataType.Password)]
public string Password { get; set; } = string.Empty;
}Sensitive fields behave as follows:
- The input renders as a password field (
*****). A placeholder indicates whether a value is currently stored. - The real value is never returned by the API. When a value is stored, the API returns
"***". When nothing is stored, it returnsnull. - Submitting an empty value is treated as "no change". To remove a stored sensitive value, use the reset button.
- The reset button appears when a sensitive value is stored. Resetting deletes the stored value and lets the underlying configuration layer (appsettings.json, environment variable) take effect again without a restart.
- Sensitive values are masked in the UI and the API only. The store keeps them as plain text, so restrict access to the database accordingly.
Use [Display] from System.ComponentModel.DataAnnotations to control how options appear in the UI.
using System.ComponentModel.DataAnnotations;
[Display(Name = "E-mail", Description = "Outgoing mail server settings")]
public class EmailOptions
{
[Display(Name = "SMTP Server", Description = "Hostname of the mail server, e.g. smtp.gmail.com")]
public string SmtpServer { get; set; } = string.Empty;
[Display(Name = "Sender address")]
public string SenderEmail { get; set; } = string.Empty;
[Display(Name = "Credentials")]
public Credentials Credentials { get; set; } = new();
}Name overrides the label shown next to the field or in the section header. When omitted, the property name is used as-is.
Description renders a small info icon next to the label. Hovering over it shows the description as a tooltip. Works on both fields and sections.
[Display] on enum members controls the label shown in the dropdown. The underlying member name is still used as the stored value.
public enum StorageProvider
{
[Display(Name = "Amazon S3")]
S3,
[Display(Name = "Azure Blob Storage")]
AzureBlob,
[Display(Name = "Google Cloud Storage")]
Gcs,
}// Data annotations
builder.Services.AddOptions<EmailOptions>().ValidateDataAnnotations();
// Custom validator
builder.Services.AddSingleton<IValidateOptions<EmailOptions>, EmailOptionsValidator>();Errors are shown in the UI and saving is blocked until they are resolved.
Kododo.ConfigWay.FeatureManagement adds a Feature flags view to the editor. You can turn Microsoft.FeatureManagement flags on and off, roll them out to users and groups, or limit them to a time window without redeploying. Your code keeps using IFeatureManager, [FeatureGate] and the other feature management APIs.
dotnet add package Kododo.ConfigWay.FeatureManagementbuilder.AddConfigWay(x =>
{
x.UsePostgreSql(connectionString);
x.AddUiEditor();
x.AddFeatureFlags();
});
builder.Services.AddFeatureManagement();The view lists the flags from your configuration (the feature_management section of appsettings, environment variables and so on), including flags in the older FeatureManagement schema. You can also add flags that exist only in ConfigWay.
A flag saved in the editor is stored by ConfigWay and replaces the flag with the same id from your configuration. Resetting it brings the configured flag back. A flag that exists only in ConfigWay is deleted on reset.
| Filter | Editor |
|---|---|
Microsoft.Percentage |
Slider |
Microsoft.TimeWindow |
Start and end in your time zone, stored in UTC |
Microsoft.Targeting |
Users, groups with a percentage, default percentage, exclusions |
| Other filters | JSON parameters |
A filter that your application has not registered can't be saved, because checking the flag would throw.
Variants, allocation, telemetry and recurring time windows are kept when you save a flag, but you can't edit them yet.
To see how a flag evaluates for a user, enter the user and groups under "Check for a user". The check uses your application's feature manager.
The flag endpoints are part of the editor's API ({path}/api) and use the same authorization. If the app runs as several replicas, set ReloadInterval so that a changed flag reaches all of them.
Outside the Development environment the editor and its API answer 403 until you decide who may use them:
app.UseConfigWay()
.RequireAuthorization(policy => policy.RequireRole("Admin"));
// or, for a public demo:
app.UseConfigWay().AllowAnonymous();A fallback authorization policy counts as a decision too. An editor nobody decided on is reported in the log at startup.
The editor runs in Way.Shell, the UI shell shared by the Kododo Way libraries: the layout, theme, language and session handling come from the shell, and with other Ways in the same app it can also be opened from the shell's panel:
builder.Services.AddWayShell(o =>
{
o.Title = "Acme";
o.HomeUrl = "~/";
o.User = ctx => ctx.User.Identity?.IsAuthenticated == true ? new ShellUser(ctx.User.Identity.Name!) { SignOutUrl = "~/logout" } : null;
});
app.UseConfigWay("/config").RequireAuthorization("Admin");
app.UseWayShell("/admin").RequireAuthorization(); // every Way the user may openUse
IOptionsSnapshot<T>orIOptionsMonitor<T>in your code to always get the latest values after a reload.
A save reloads the configuration in the instance that handled it. When the app runs as several replicas sharing one store, let each of them check the store for changes:
builder.AddConfigWay(x =>
{
x.UsePostgreSql(connectionString);
x.ReloadInterval = TimeSpan.FromSeconds(30);
});Every 30 seconds each replica reads the store and reloads only when a setting was added, changed or removed, so the other replicas get a change within one interval. Without ReloadInterval they keep their values until a restart.
- Without
ReloadInterval, changes are reloaded only in the instance that saved them (see Several replicas). - Saves do not check for concurrent edits. If two people edit the same section at the same time, the last save wins.
You can replace the built-in in-memory store with any backend by implementing IStore from Kododo.ConfigWay.Core:
builder.AddConfigWay(x =>
{
x.Store = new MyCustomStore(); // implements IStore
x.AddOptions<AppOptions>();
});MIT




