Settings of your own: custom settings
An application usually has settings of its own: a shop's currency, a payment provider's key, the
address orders are mailed to. Register them with the server, and they get a tab in the admin UI
(beside Server settings, or beside each database's Settings) with the same editors, tags and
layers as the built-in settings. Code reads them with db.Settings.Get<T>().
A set of settings is a plain class and a definition that implements ICustomSettingsDefinition<T>.
The class must be public, have a parameterless constructor and serialize with System.Text.Json.
public class WebshopSettings {
public string Currency { get; set; } = "NOK"; // class defaults are the bottom layer
public decimal FreeShippingAbove { get; set; } = 500;
public string? PaymentApiKey { get; set; }
public ShippingZone[] Zones { get; set; } = [];
}
public class ShippingZone {
public Guid Id { get; set; } // list elements need a Guid Id
public string Name { get; set; } = "";
public decimal Price { get; set; }
}
public class WebshopSettingsDefinition : ICustomSettingsDefinition<WebshopSettings> {
public string FileKey => "webshop"; // names its files and config section
public string Title => "Webshop"; // the tab
public CustomSettingsScope Scope => CustomSettingsScope.Database; // or Server: one set for the server
public string? Description => "How the shop on this database sells.";
public string Icon => "shop";
public SettingGroupDefinition[] Groups => [
new() { Id = "prices", Title = "Prices", Settings = [
new() { Path = "Currency", Label = "Currency", Applies = SettingApplies.Live, Help = "The ISO code prices are shown in." },
new() { Path = "FreeShippingAbove", Label = "Free shipping above", Applies = SettingApplies.Live, Help = "Orders above this ship free." },
new() { Path = "PaymentApiKey", Label = "Payment API key", Secret = true, Applies = SettingApplies.Live, Help = "Keep it in configuration." },
] },
new() { Id = "zones", Title = "Shipping zones", List = new() {
Path = "Zones", ItemName = "zone", LabelField = "Name", EmptyHelp = "No zones.",
Fields = [ new() { Path = "Name", Label = "Name", Help = "" }, new() { Path = "Price", Label = "Price", Help = "" } ],
} },
];
// optional: the code layer, run at every start; what it sets is locked on the tab and never saved
public void OnInit(WebshopSettings settings, NodeStoreContainerSettingsBase? database) { }
}builder.AddRelatudeDB(options => {
options.AddCustomSettings(new WebshopSettingsDefinition());
});
// a library registers the definition as a service instead:
// builder.Services.AddSingleton<ICustomSettingsDefinition, WebshopSettingsDefinition>();
var shop = db.Settings.Get<WebshopSettings>(); // this database's set, or the server's for a Server set
var site = RelatudeDBRuntime.Server.CustomSettings.Get<SiteSettings>(); // with no store at handGroups uses the same SettingGroupDefinition and SettingDefinition classes as the built-in
settings pages:
Pathis the property path into your class ("Shipping.FreeAbove"for a nested object). The editor, an enum's choices and the default are read from the property.Secret,ReadOnly,Unit,Placeholder,VisibleWhen,HiddenWhen,Suggestions,Editor = SettingEditor.Colorand list groups work as they do on the built-in pages.Appliesdefaults toReopen. SetLivefor a value your code reads each time it uses it.- Leave
Groupsout, and the tab shows every property in one group, labelled from[DisplayName]and[Description].[PasswordPropertyText(true)]marks a secret.
The layers, each merged over the one before at every start and soft restart:
| Layer | Server set | Set per database |
|---|---|---|
| defaults | new T() |
new T() |
| SHARED | relatude.settings/{FileKey}.json |
relatude.settings/{short name}/{FileKey}.json; relatude.settings/{FileKey}.json without a short name |
| THIS SERVER | overrides/{FileKey}.overrides.json, beside relatude.db.overrides.json |
overrides/{FileKey}.overrides.json in the database's storage |
| APPSETTINGS | RelatudeDB:CustomSettings:{FileKey} |
that for every database, then RelatudeDB:CustomSettings:Databases:{short name or id}:{FileKey} |
| CODE | OnInit |
OnInit, told which database |
The SHARED file needs only what differs from the class defaults, and it may have comments. The tab saves to THIS SERVER. Move to shared moves entries into the SHARED file, which is rewritten whole, so its comments are lost. Values from configuration and code are never saved, so a key in user secrets stays out of every file:
dotnet user-secrets set "RelatudeDB:CustomSettings:webshop:PaymentApiKey" "sk_live_…"Get<T>() returns the settings object in force. A save from the tab replaces that object rather than
changing it. So call Get<T>() each time you need the values (it is a cheap lookup), and treat the
result as read only.
Mistakes show early:
- A wrong definition stops the start with a message saying what is wrong: a file key that cannot
name a file, two sets with the same key or class, or a
Paththat names no property. - A SHARED or THIS SERVER file that is not valid JSON makes
Get<T>()throw, naming the file. It does not fall back to the defaults.