Skip to content
kododo-devPublic

About

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.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Latest commit

 

History

47 Commits

Folders and files

Repository files navigation

ConfigWay

CI Demo

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.

UI

Overview

Field types

Array editor

Feature flags

Flag editor

Packages

Package NuGet Description
Kododo.ConfigWay NuGet Core DI registration and in-memory store
Kododo.ConfigWay.Core NuGet Abstractions and interfaces (for extension authors)
Kododo.ConfigWay.UI NuGet Embedded web UI
Kododo.ConfigWay.PostgreSQL NuGet PostgreSQL persistence store
Kododo.ConfigWay.SqlServer NuGet SQL Server persistence store
Kododo.ConfigWay.FeatureManagement NuGet Feature flags of Microsoft.FeatureManagement in the editor

Quick start

dotnet add package Kododo.ConfigWay
dotnet add package Kododo.ConfigWay.UI
dotnet add package Kododo.ConfigWay.PostgreSQL  # or Kododo.ConfigWay.SqlServer — optional, for persistence
builder.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

Registering options

// 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.

Supported field types

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.

Array types

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.

Reset to default

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.

Sensitive fields

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 returns null.
  • 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.

Customizing UI labels and descriptions

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,
}

Validation

// 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.

Feature flags

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.FeatureManagement
builder.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.

Security

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.

Way.Shell

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 open

Use IOptionsSnapshot<T> or IOptionsMonitor<T> in your code to always get the latest values after a reload.

Several replicas

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.

Limitations

  • 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.

Custom store

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>();
});

License

MIT

About

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.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Contributors

Languages