Relatude Help

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 hand

Groups uses the same SettingGroupDefinition and SettingDefinition classes as the built-in settings pages:

  • Path is 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.Color and list groups work as they do on the built-in pages.
  • Applies defaults to Reopen. Set Live for a value your code reads each time it uses it.
  • Leave Groups out, 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 Path that 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.