Dependency injection

Services are wired at compile time. A class declares its lifetime by naming a marker, inject T asks for one, and the compiler builds the graph: there is no container at run time, no registration call and no reflection.


Marking a service #

class Clock : IClock, IServiceSingleton { ... }

IServiceSingleton, IServiceScoped and IServiceTransient are markers, not base classes and not interfaces: a class names one of them in its : list, alongside the interfaces it really implements. Naming two of them is refused, and an abstract class is not a service.

markerone instance per
IServiceSingletonthe program — a root, allocated once and never released
IServiceScopedthe enclosing scope { }, or the private scope of the singleton that pulled it in
IServiceTransientevery injection

A class derived from a service is not itself a service: the marker does not inherit, so such a class answers no key and is built with a plain new.

Asking for one #

IClock a = inject IClock;      // an interface the service implements
Clock  b = inject Clock;       // the service's own name
IBase  c = inject IBase;       // an interface that interface extends
app.ICache d = inject app.ICache;   // qualified, or bare under a `using`

A service answers to every one of those keys, and they all land on the same instance for a Singleton. If no class implements the type, the site is teko: no service implements this type; if two do, teko: two services implement this interface: ....

Constructor injection #

A service that needs others declares them as constructor parameters; the compiler fills them in, recursively, and a parameter with a default that is not a service is filled from that default. A service has exactly one injectable constructor — two taking the same number of injectable parameters is refused — and its constructor has to be reachable (private/protected is teko: the constructor of this service is not accessible). A cycle in the graph is teko: cyclic service: ....

// expect-exit: 42
#include "rt.tk"

interface IClock {
    i64 tick();
}

interface IDb {
    i64 query();
}

interface IRepo {
    i64 load();
}

class Clock : IClock, IServiceSingleton {
    i64 n;

    public i64 tick() {
        n = n + 1;
        return n;
    }
}

class Db : IDb, IServiceSingleton {
    i64 n;

    public Db(i64 seed = 5) {                    // not a service: the default fills it
        n = seed;
    }

    public i64 query() {
        n = n + 1;
        return n;
    }
}

class Repo : IRepo, IServiceSingleton {
    IDb db;

    public Repo(IDb db) {                        // injected by constructor
        this.db = db;
    }

    public i64 load() {
        return this.db.query();
    }
}

class Svc : IServiceSingleton {
    IRepo repo;
    IClock clock;

    public Svc(IRepo repo, IClock clock) {
        this.repo = repo;
        this.clock = clock;
    }

    public i64 run() {
        return this.repo.load() + this.clock.tick();
    }
}

i64 main() {
    IClock a = inject IClock;
    Clock b = inject Clock;                      // the same instance, by its own name
    if (a.tick() != 1) return 1;
    if (b.tick() != 2) return 2;

    IRepo r = inject IRepo;                      // through the interface: the table answers
    if (r.load() != 6) return 3;                 // Db's own default (5), then one query

    Svc s = inject Svc;                          // built from IRepo and IClock
    if (s.run() != 10) return 4;                 // the same Repo and Clock

    if (rt_live() != 4) return 5;                // Clock, Db, Repo, Svc -- never more
    return 42;
}

scope { } #

scope { ... } opens a lexical scope. Inside it a IServiceScoped service is one instance shared by every injection of that scope, and it is released at the } — including when a return leaves the block early. Two scopes in a row build two instances; a scope nested in another builds its own and leaves the outer one untouched. A IServiceTransient built inside a scope is fresh per injection, and its own scoped dependency is that scope's shared instance.

// expect-exit: 42
#include "rt.tk"

i64 dtors = 0;

interface IAudit {
    i64 hit();
}

class Audit : IAudit, IServiceScoped {
    i64 n;

    public i64 hit() {
        n = n + 1;
        return n;
    }

    ~Audit() {
        dtors = dtors + 1;
    }
}

class Job : IServiceTransient {
    IAudit a;

    public Job(IAudit a) {
        this.a = a;
    }

    public i64 use() {
        return this.a.hit();
    }
}

i64 main() {
    scope {
        IAudit a1 = inject IAudit;
        IAudit a2 = inject IAudit;
        if (a1.hit() != 1) return 1;
        if (a2.hit() != 2) return 2;             // the SAME instance
        if (rt_live() != 1) return 3;
    }
    if (rt_live() != 0) return 4;
    if (dtors != 1) return 5;                    // released at the `}`

    dtors = 0;
    scope {
        IAudit b1 = inject IAudit;
        if (b1.hit() != 1) return 6;             // a fresh instance, counting from 1
        scope {
            IAudit inner = inject IAudit;
            if (inner.hit() != 1) return 7;      // its own, again
            if (rt_live() != 2) return 8;
        }
        if (rt_live() != 1) return 9;            // the inner one is gone
        if (b1.hit() != 2) return 10;            // the outer one is untouched
    }

    dtors = 0;
    scope {
        Job j1 = inject Job;
        Job j2 = inject Job;                     // a fresh Job per injection
        if (j1.use() != 1) return 11;
        if (j2.use() != 2) return 12;            // sharing the scope's one Audit
        if (rt_live() != 3) return 13;           // the Audit and both Jobs
    }
    if (rt_live() != 0) return 14;
    if (dtors != 1) return 15;
    return 42;
}

A Singleton may receive a Scoped #

This is where teko differs from C# on purpose: a Singleton has a scope of its own, so a Scoped service may be injected into it. The rules that follow from that:

// expect-exit: 42
#include "rt.tk"

class Meter : IServiceScoped {
    i64 n;

    public i64 tick() {
        n = n + 1;
        return n;
    }
}

class WrapA : IServiceTransient {
    Meter m;

    public WrapA(Meter m) {
        this.m = m;
    }

    public i64 read() {
        return this.m.tick();
    }
}

class WrapB : IServiceTransient {
    Meter m;

    public WrapB(Meter m) {
        this.m = m;
    }

    public i64 read() {
        return this.m.tick();
    }
}

class Board : IServiceSingleton {
    WrapA a;
    WrapB b;

    public Board(WrapA a, WrapB b) {
        this.a = a;
        this.b = b;
    }

    public i64 total() {
        return this.a.read() + this.b.read();
    }
}

class GaugeX : IServiceSingleton {
    Meter m;

    public GaugeX(Meter m) {
        this.m = m;
    }

    public i64 read() {
        return this.m.tick();
    }
}

class GaugeY : IServiceSingleton {
    Meter m;

    public GaugeY(Meter m) {
        this.m = m;
    }

    public i64 read() {
        return this.m.tick();
    }
}

i64 main() {
    Board brd = inject Board;
    if (brd.total() != 3) return 1;              // 1 + 2: ONE Meter, shared
    if (rt_live() != 4) return 2;                // Meter, WrapA, WrapB, Board

    GaugeX gx = inject GaugeX;
    GaugeY gy = inject GaugeY;
    if (gx.read() != 1) return 3;                // its OWN Meter, from 1
    if (gy.read() != 1) return 4;                // a different one, also from 1
    if (rt_live() != 8) return 5;
    return 42;
}

Lifetime compatibility #

a …may receive a Singletona Scopeda Transient
Singletonyesyes — its own scope's instanceyes
Scopedyesyes — the same scope's instanceyes
Transientyesyes — the scope it was injected intoyes

A inject of a Scoped service written outside any scope { } and outside a service's own graph has no scope to belong to, and an inject written inside a lambda takes the service of the enclosing scope: bind it outside the lambda and capture it with use (...).


Limits #

limitvalue
a generic key (IRepo<T>)not taught
a keyed or named registrationnot taught
a factory, or a decorationnot taught
a service that is not a classrefused (a service marker names a class)
an abstract class as a servicerefused
resolving a service by a call (Services.Get<T>())there is no such surface: the form is a marker plus inject
classes marked with a lifetime32
inject sites32
scope { } blocks (plus one per singleton)64
services under construction at once (the cycle guard)32

Edit this page