Skip to content
Beskid Platform specification

Beskid

Jump to a Beskid service

Beskid

Jump to a Beskid service

Native dependency injection - Design model

Platform spec article

Native dependency injection - Design model

Spec standingStandard

Owner
Piotr Mikstacki
Submitter
Piotr Mikstacki
ConstructRole
hostNamed composition root; may extend another host or corelib base
registry { }Registrations at the global level for that host
scope Name(params) { }Named child container under global or parent scope
startup(...)Runs once per launch at global only
init(...)Runs once per with activation, before the block body
dispose(...)Runs once per activation leave, after the block body (or on unwind)
with Name(args) { }Activates a named scope
launch Host(args)Starts exactly one host instance for the process run
injectField injection on ordinary types (not constructors)

Declare scopes on the host. Activate with with. v0.2 uses manual with and ConsoleHost only; WebHost is v0.3+.

Composition uses a fixed mental model with two layers:

The global scope is the merged registry of the launched host after walking the host inheritance chain (: ParentHost … down to corelib bases such as ConsoleHost). It is not a separate syntax block.

  • Always present for the lifetime of a launch.
  • startup and unqualified inject resolve here when no active named scope provides a binding.
  • Treat global + its host parent chain as the root of the scope tree.

scope blocks form a tree under the host:

  • Top-level scope children attach to global.
  • Nested scope children attach to their parent scope.
  • Registrations in a named scope default to per-activation (scoped) unless marked single or transient.

During execution the compiler maintains a scope stack (fiber-local; see Execution):

[ Global, … optional named scopes from outer with … innermost with ]

Resolution (default): walk from innermost named scope toward global; first matching binding wins for singular inject. Plural inject collects all matching registrations at the lowest stack level that has at least one registration for that contract (see below).

FormMeaning
Unqualified inject T fieldInnermost → … → global
inject global::T fieldGlobal registry only (skip named scopes)
inject parent::T fieldParent scope on the stack (one level up from innermost named scope; global if innermost is a top-level named scope)

init, dispose, and startup parameters may use the same qualifiers where injection applies.

Projects may declare many named host types. host is named so libraries and apps can define reusable composition roots.

RuleDetail
DefinitionsAny number of host AppHost, host ApiHost, etc. in a compilation
LaunchExactly one host per process run via launch SomeHost(args)
ManifestThe executable app target names the host entry (see Contracts); a workspace must not launch two apps in one run
Lib projectsMay declare host types for reuse; launch is forbidden on Lib targets
host InfraHost : ConsoleHost {
registry {
single SharedConfig for Configuration;
}
}
host AppHost(string[] args) : InfraHost {
registry {
single AppConfig for Configuration; // overrides InfraHost / ConsoleHost for Configuration
single SqlStorage for Storage;
single FileStorage for Storage;
}
// ...
}

Merge order: start at the root base (for example ConsoleHost), apply each : ParentHost toward the launched host. The launched host is topmost — its registry entries override the same contract or self-bind type key from parents.

Override key: contract type for for Contract lines; implementation type for self-bound lines.

Lifetime mismatch on override (parent single, child transient for the same key) is a compile error.

host AppHost(string[] args) : ConsoleHost {
registry {
single AppConfiguration for Configuration;
single SqlStorage for Storage;
single FileStorage for Storage;
transient DefaultLogger for Logger;
}
scope HttpScope(RequestContext request) {
ScopedDbSession for DbSession;
OIDCAuthService for AuthService;
init(
global::Configuration configuration,
AuthService auth,
) {
// per activation
}
dispose(DbSession db, AuthService auth) {
auth.SignOut();
db.Close();
}
scope UnitOfWork(bool readOnly) {
ScopedTransaction for Transaction;
init(Transaction tx) {
tx.Begin(readOnly);
}
dispose(Transaction tx) {
tx.Commit();
}
}
}
startup(
Configuration configuration,
Storage[] storages,
) {
// global only; named scopes not active
}
}

Parameters on host AppHost(...) are launch inputs only. launch AppHost(args) must match that list.

KeywordMeaning
singleOne instance per launch (process-wide for that host run)
transientNew instance on each resolve at global
single Implementation for Contract;
transient Implementation;
single ConcreteType;

Multiple implementations for one contract at the same container level are allowed. Consumers use array field injection (below).

KeywordMeaning
(omitted)Scoped — one instance per with activation
singleOne instance per activation of this named scope
transientNew instance on each resolve while the scope is active

Constructor inject is forbidden. Dependencies are fields only:

type Aggregator {
inject Configuration configuration;
inject Storage[] storages; // all Storage registrations in resolution container
inject global::Logger logger; // force global registry
}
inject DbSession session;

Must resolve to exactly one registration in the walked scope chain. If zero → error; if many at the same level → error directing the author to array inject.

inject Storage[] storages;
inject StorageProvider[] providers;

Must resolve to one or more registrations for that contract (or self-bind type) at the resolution container for the inject site. Order is registration order in the merged container (deterministic): base hosts first, then parent hosts, then launched host; within a registry, source order.

startup, init, and dispose may use plural parameters the same way.

  • init — after activation instances are created, before the with body.
  • dispose — after the with body on normal completion, and on activation unwind; parameters name services to tear down; order is reverse of init parameter order unless a future ordering attribute is added.
  • Failures in init prevent the body from running; dispose still runs for activations that completed init per unwind rules.
with HttpScope(request) {
with UnitOfWork(readOnly: false) {
Save();
}
}
  • Nested with on the same scope name creates independent activations (separate scoped instance sets).
  • Child named scopes must have parent active on the stack.
unit main(string[] args) {
launch AppHost(args);
}

Lowering must:

  1. Merge host chain → global container for the launched host.
  2. Run startup (global only).
  3. Enter base host run loop (ConsoleHost in v0.2).

The project manifest app executable target is the single application entry: it points at main (or equivalent) that performs exactly one launch. Running two launch calls in one process is forbidden.

A Lib assembly may declare:

host InfraHost : ConsoleHost {
registry { ... }
}

An app host composes on top:

host AppHost : InfraHost { registry { ... } }

launch in a Lib target is a compile error.

EntityRole
Host chainOrdered hosts from base → launched
Global containerMerged registry after overrides
Scope treeNamed scopes and parent links
Binding planField slots, array aggregates, with enter/leave
Composition snapshotFrozen graph for mods after composition.resolve
  • Constructor injection.
  • Runtime service locator.
  • Attribute-driven scope activation.
  • launch on Lib or dual-app processes.
  • WebHost automatic request scope (v0.3).
  • host on Mod projects.