Summary#
A package declares WHAT it needs as a class; the app supplies it; the package reads it.
interface IMailer {
string Deliver(string to, string body);
}
class LogMailer : IMailer {
public string Prefix = "log";
public string Deliver(string to, string body) { return Prefix + " → " + to; }
}
class RelayMailer : IMailer {
public string Host = "";
public string Deliver(string to, string body) { return "relay " + Host + " → " + to; }
}
// The package's half: the setting, with its defaults.
[AppSetting("Mail")]
class MailSetup {
public List<IMailer> Mailers = [];
public string Signature = "— sent by Osy#";
}
// The app's half: which mailers, and how each is configured.
app.Mail = new MailSetup {
Mailers = [ new RelayMailer { Host = "mail.example.org" }, new LogMailer { Prefix = "audit" } ],
};
// Code that uses it — in the package, or in the app.
string SendToAll(string to) {
var mail = App.Setting<MailSetup>();
var receipts = "";
foreach (var mailer in mail.Mailers) {
receipts = receipts + mailer.Deliver(to, "Hello" + mail.Signature) + "; ";
}
return receipts;
}Signature#
[AppSetting("<Name>")]
class <Setup> { <fields with defaults> } // a package or the app declares it
app.<Name> = new <Setup> { … }; // the app sets it — once
app.Configure(new <Setup> { … }); // …or the same, named by its TYPE
var s = App.Setting<<Setup>>(); // any server code reads it
[AppSetting("<Name>", Browser = true)] // …and pages too: a render, an action, `on mount`
class <Words> { <text, numbers, enums, lists and classes of them> }Description#
Who writes which half?#
| written by | what it says | |
|---|---|---|
[AppSetting("Mail")] class MailSetup | the package that needs configuring (or the app) | the setting's name and shape, and its defaults |
app.Mail = new MailSetup { … }; | the app | this app's value |
App.Setting<MailSetup>() | the package's code, or the app's | read the value |
The name in [AppSetting("…")] is the app. property the app writes. It must be an identifier, and may not be a
built-in config property (app.Secrets, app.Auth, …) or Configure.
Two packages that both call theirs Mail#
A setting IS its class: App.Setting<MailSetup>() reads by type, and the app's value is kept by the class's full name.
So two packages may each declare a setting called Mail, and an app may use both. The short form app.Mail = … is
then ambiguous — the compiler refuses it and offers the other form for each — and you name each by its type, the way
ASP.NET's options pattern does:
app.Configure(new Acme.Mail.MailSetup { Mailers = [ new RelayMailer { Host = "mail.example.org" } ] });
app.Configure(new Globex.Mail.MailSetup { From = "[email protected]" });app.Configure(new T { … }) works for every setting, ambiguous or not; write T in full when two settings' classes
share a name. Either spelling sets the setting once: app.Mail = … and app.Configure(new MailSetup { … }) for the
same class are one setting set twice, and refused as such.
What happens when the app sets nothing?#
App.Setting<MailSetup>() answers new MailSetup() — the field initializers are the defaults. So the class needs a
parameterless constructor (having none at all counts). A package's own tests see exactly this, because there is no
app there to set anything.
[AppSetting("Paging")]
class PagingSetup {
public int PageSize = 25;
}
int PageSize() { return App.Setting<PagingSetup>().PageSize; }Can the value hold plug-ins from other packages?#
Yes — that is what it is for. The value is an ordinary object: a field typed as an interface, or a List of one,
holds any class that implements it, and a call on it runs that class's code
(interface dispatch). So a contract package can declare IMailer, a package per provider can
implement it without knowing the setting exists, and the app lists the ones it wants — with an implementation of its
own beside them if it likes. Adding a provider is then a package and one line in app.Mail, with no change to the
package that sends mail.
When is the value worked out?#
Once. An app.X = … is configuration: the platform works it out the first time it is read in an app version, and
keeps it. Every later App.Setting<T>() answers from what was kept — no statement, however many places read it — and
each reader gets its own copy, so code that changes the object it was handed never changes what the next reader
gets.
It is worked out as the app: with no signed-in caller, on the app's own authority. So the value is the same whoever asks, and one caller's view of the data is never handed to another. Three things change how long it is kept:
| the value… | it is |
|---|---|
| reads nothing (literals, providers, the defaults) | kept for the whole app version |
reads a row, or a Secret.X | kept until a table it read is written — the next read works it out again |
| has text a person reads in it | kept per language, worked out in the reader's |
reads WHO is asking (Session.CurrentUser, Visitor) or WHEN (a clock, the request) | worked out as the caller, once per unit of work, never shared |
A setting that reads data#
It works, and the compiler warns — APP_SETTING_READS_DATA, on the line — because every write to the table it read
throws the whole value away. There are two cheaper ways to say the same thing:
- A member that is a function is worked out where it is USED — as whoever is asking, every time — so a lambda is the place for a read that must be fresh:
entity Office {
[MaxLength(200)] string Signature;
security { allow read when IsAuthenticated || IsAnonymous; }
}
[AppSetting("Letters")]
class LettersSetup {
public string Greeting = "Hello";
public Func<string>? Signature;
}
app.Letters = new LettersSetup {
Greeting = "Dear reader",
// Not read here: read by whoever calls `Signature()`, when they call it.
Signature = () => Office.FirstOrDefault()?.Signature ?? "",
};
int SignatureLength() { return App.Setting<LettersSetup>().Signature!().Length; }- Better — leave the read out of the setting, and let the code that needs the record read it itself. A package that owns a settings record reads it where it uses it; the app's setting then says only what the app CHOOSES.
app.Letters = new LettersSetup {
Greeting = Office.FirstOrDefault()?.Signature ?? "", // ⚠ APP_SETTING_READS_DATA
};osy kits check makes the same rule a gate for a kit — KIT_SETTING_READS_DATA (MUST): neither the defaults of a
setting it declares nor its test app's app.X reads a record, because a kit's test app is the setup every app on it
copies.
Are secrets safe in a setting?#
A provider's credentials belong in the setting, written as Secret.X:
app.Secrets = [ new Secret("RelayPassword") ];
interface ISigner { string Sign(string message); }
class HmacSigner : ISigner {
public string Key = "";
public string Sign(string message) {
return Crypto.ToHex(Crypto.HmacSha256(Text.ToBytes(Key), Text.ToBytes(message)));
}
}
[AppSetting("Signing")]
class SigningSetup { public List<ISigner> Signers = []; }
app.Signing = new SigningSetup { Signers = [ new HmacSigner { Key = Secret.RelayPassword } ] };
string SignWithAll(string message) {
var signed = "";
foreach (var signer in App.Setting<SigningSetup>().Signers) { signed = signed + signer.Sign(message); }
return signed;
}Two things keep that safe:
- A rotated secret takes effect on the next read. The value is kept only until a table it read is written, and
setting a secret writes one — so the next
App.Setting<T>()works the value out again with the new key. AUserScopedsecret belongs to whoever is asking, so a value that reads one is worked out as each caller. - The setting is confined like a secret. Code may CALL methods on it and on what it holds, and return what those
methods answer (
signer.Sign(message)above). It may not return the setting, or a text field read off it, from a function; store it in an entity; log it; or read it in anything the browser runs (anaction, a render). Each is a compile error saying so — see reading a secret's value (Secret.Name) for the same rules on a secret.
Can a page read a setting?#
Only one its class says is page material: [AppSetting("Storefront", Browser = true)]. Then a render slot, an
action and an on mount read App.Setting<T>() exactly as server code does — and none of them waits on the server,
because the value rides the page's boot answer (the one that carries the page's Config.X values).
A server render of a public page paints it too.
// The package's half — what a storefront shows, with its defaults.
public enum Shade { Light, Dark }
public class Banner {
public string Text = "";
public Shade Shade = Shade.Light;
}
[AppSetting("Storefront", Browser = true)]
public class StorefrontWords {
public string Greeting = "Welcome";
public List<string> Tips = [];
public Banner Banner = new Banner();
}
// The app's half.
app.Storefront = new StorefrontWords {
Greeting = "Hello, shopper",
Tips = ["Free returns", "Ships in a day"],
Banner = new Banner { Text = "Summer sale", Shade = Shade.Dark },
};
[Route("/")] [AllowAnonymous]
component Storefront() {
string banner = "";
action ShowBanner() { banner = App.Setting<StorefrontWords>().Banner.Text; }
render {
Stack {
Text(App.Setting<StorefrontWords>().Greeting);
foreach (var tip in App.Setting<StorefrontWords>().Tips) { Text(tip); }
Button("Today's offer", onPress: ShowBanner);
Text(banner);
}
}
}The value is sent to every visitor's browser, so the compiler refuses a page setting — APP_SETTING_NOT_PAGE_MATERIAL
— that could hold anything a visitor may not see:
| in the setting | why it is refused |
|---|---|
a member typed as an interface (IMailer), at any depth | any class may stand behind it, one holding a credential included |
a delegate (Func<…>, Action<…>) member, or a lambda in the value | code, which a credential can hide inside |
a member that is a stored row (Product) | whether a visitor may read a row is that row's security { }'s call |
a Secret.X or OAuth token read, at any depth of call | a credential never leaves the server |
a read of a setting NOT marked Browser = true | it may hold credentials |
| a read of stored data | it is worked out once, as the app — a visitor would see rows their security refuses |
who is asking, or when (Session.CurrentUser, a clock) | one value for every visitor; read that on the page itself |
Text a person reads is fine — the value is worked out in the reader's language, like any setting.
What does the compiler refuse?#
| you wrote | what it says |
|---|---|
app.Mail = new LogMailer { … }; | app.Mail is a MailSetup setting, so its value must be a MailSetup (or a class derived from it) |
app.Mial = … | not a config property — did you mean app.Mail? — listing the settings declared by the app and its packages |
app.Mail = … twice, or it and app.Configure(new MailSetup { … }) | set more than once — an app setting has one value |
app.Mail = … when two settings are called Mail | claimed by 2 settings — offering app.Configure(new <each> { … }); |
app.Configure(new LogMailer { … }) | LogMailer is not an app setting — listing the settings there are |
App.Setting<LogMailer>() | LogMailer is not an app setting — mark it [AppSetting("…")] or read one that is |
[AppSetting("Mail")] on an entity, an interface or an abstract class | a setting is a class that can be created |
return App.Setting<MailSetup>(); | a setting may hold credentials, so a function cannot return it |
App.Setting<MailSetup>() in a render or an action | a setting may hold credentials, so the browser may not read it — unless its class is marked [AppSetting("Mail", Browser = true)] ([[config-app-settings#pages]]) |
[AppSetting("Words", Browser = 1)], or another named argument | Browser takes true or false, and is [AppSetting]'s only named argument |
a Browser = true setting holding an interface, a delegate, a row, a secret or data | APP_SETTING_NOT_PAGE_MATERIAL, naming what and where ([[config-app-settings#pages]]) |
app.Mail = new MailSetup { Signature = Office.First().Signature }; | a WARNING, APP_SETTING_READS_DATA: it reads data to work out its value — use a Func<…> member, or read the record where it is used ([[config-app-settings#reads-data]]) |
Examples#
Reading the setting from the app's own code as well as the package's — the same call everywhere:
int MailerCount() { return App.Setting<MailSetup>().Mailers.Count; }Naming the setting by its type — the form that always works:
[AppSetting("Paging")]
class PagingSetup {
public int PageSize = 25;
}
app.Configure(new PagingSetup { PageSize = 50 });
int PageSize() { return App.Setting<PagingSetup>().PageSize; }See also#
- Interfaces — a contract several types can satisfy — the contract a list of plug-ins is typed as
- declaring secrets (app.Secrets) — declaring the
Secret.Xa provider's settings name - data held somewhere else (app.DataSources) — the platform's own provider registry, built fresh from its entry the same way
- Sequence fields on a class —
List<T>and collection literals in an initializer