Osy#betaRun it on Osyrin: osyrin.com

Reference / Config

settings a package asks its app for (app settings)

[AppSetting("Name")] class <Setup> { … } [AppSetting("Name", Browser = true)] app.Name = new <Setup> { … }; app.Configure(new <Setup> { … }); App.Setting<Setup>()

A package — or an app — declares a setting by marking a class `[AppSetting("Name")]`. The app that uses it fills it in with `app.Name = new Setup { … };` — or `app.Configure(new Setup { … });`, which names it by its type — and code reads it with `App.Setting<Setup>()`. The value can hold objects of any class — including implementations of an interface, from other packages or the app itself — so this is how a package calls plug-ins its app chooses. Unset, a setting is `new Setup()`. The value is configuration: worked out once, as the app, and kept. A setting marked `Browser = true` is page material: a page reads it too, with no round trip, and the compiler proves it holds nothing a visitor may not see.

stable7 examples compiled by CIconfigpackagesprovidersclasses

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 bywhat it says
[AppSetting("Mail")] class MailSetupthe package that needs configuring (or the app)the setting's name and shape, and its defaults
app.Mail = new MailSetup { … };the appthis app's value
App.Setting<MailSetup>()the package's code, or the app'sread 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.Xkept until a table it read is written — the next read works it out again
has text a person reads in itkept 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:

  1. 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; }
  1. 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. A UserScoped secret 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 (an action, 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 settingwhy it is refused
a member typed as an interface (IMailer), at any depthany class may stand behind it, one holding a credential included
a delegate (Func<…>, Action<…>) member, or a lambda in the valuecode, 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 calla credential never leaves the server
a read of a setting NOT marked Browser = trueit may hold credentials
a read of stored datait 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 wrotewhat 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 Mailclaimed 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 classa 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 actiona 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 argumentBrowser 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 dataAPP_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#

Related

Interfaces — a contract several types can satisfy

An `interface` declares what a type must do without saying how — methods and property contracts, and no state. A member…

declaring secrets (app.Secrets)

`app.Secrets` declares the named secrets your app uses — API keys, tokens, client secrets. Each is `new…

data held somewhere else (app.DataSources)

`app.DataSources` registers the places an app's data can live besides its own database — a warehouse, a SaaS table API…

Sequence fields on a class

A class can hold many values in one field — `string[]`, `int[]`, `List<Tag>`, `HashSet<string>`, `Dictionary<string…