On this page

20. Dependency injection and services

20.1 Object dependencies

CS-SERVICE-001. needs declares an object's effective dependency contract. Needs are private and stable by default. Individual needs may be protected or explicitly replaceable; a protected needs block and public/internal needs are not supported. Changing a replaceable need does not re-resolve it or dispose the previous value.

public class FileReportWriter
    needs
        outputDirectory as String
    end needs

    public action Write(name as String, contents as String)
        let path = Global.System.IO.Path.Combine(outputDirectory, name)
        Global.System.IO.File.WriteAllText(path, contents)
    end action
end class

Direct construction supplies needs as named construction inputs:

let writer = new FileReportWriter() with
    outputDirectory: "/var/reports"
end with
writer.Write("daily.txt", "Daily import completed")

CS-SERVICE-002. A need without a default is required on every successful construction path. A required reference/nullable need cannot be nothing; a required nonnullable value-type need must be supplied. A need with a default may be omitted. = nothing explicitly permits absence. Defaults express optional/default values, not hidden selection of a concrete implementation.

CS-SERVICE-003. Required need validation occurs at construction and at replacement of a required replaceable need. Statically detectable missing inputs are compile-time errors. External/reflection/provider construction receives generated runtime invariant checks naming the dependency and declaring type. The programmer does not need to add an ensure merely to obtain this invariant.

20.2 Effective contracts and initialization

CS-SERVICE-004. A concrete class combines its own needs, inherited DirectSharp needs and contributed class-kind needs into one contract. Need names use case-insensitive identifier identity and must be unique across that effective hierarchy, even when a base need is private. A constructor overload does not define a different dependency contract.

Per-type construction order is:

  1. Member declaration initializers.
  2. Constructor delegation/base construction.
  3. Validate and store that type's needs.
  4. Constructor with properties assignments.
  5. Constructor body.

Stored needs must not be read by initializers that execute before those needs are established. arguments.needName provides strongly typed effective construction inputs where permitted by the construction model. Inherited DirectSharp needs are automatically forwarded. Imported CLR bases retain their ordinary explicit constructor inputs.

Cross-assembly metadata must preserve dependency names, types, keys, required/default status, visibility, replacement and declaring-type information. C# constructors remain ordinary callable CLR constructors; compiler metadata retains the stronger DirectSharp contract.

Constructor-local with needs, with properties and with fields contribute class declarations only when the class declares exactly one constructor. Multiple overloads, including delegating overloads, make these placement shortcuts invalid.

20.3 Service identity and ownership

CS-SERVICE-005. Service identity is a service type plus an optional exact key. String keys are case-sensitive; other constant keys preserve their exact typed value. This intentionally differs from case-insensitive source identifiers. The compiler must not lowercase or normalize keys.

needs
    auditLogger as Global.Microsoft.Extensions.Logging.ILogger with key "audit"
end needs

"audit" and "Audit" identify different keyed registrations. The snippet is a class member declaration with an available logging reference.

CS-SERVICE-006. Needs are borrowed. The consuming object must not dispose a declared or inherited need, including a replaceable/protected need. Ownership-taking disposal of a statically known borrowed value is diagnosed. Replacing a dependency does not dispose its previous value.

20.4 Composition and registration

CS-SERVICE-007. A services declaration is an action-like composition module. It may accept arguments, use ordinary control flow and local actions, call other composition modules and perform registrations. ServiceRegistry supplies advanced/native registration access. Local helper actions inherit the composition context; they are not escaping general runtime delegates with implicit registry capture.

services ReportingServices(outputDirectory as String)
    provide transient FileReportWriter as new FileReportWriter() with
        outputDirectory: outputDirectory
    end with
end services

CS-SERVICE-008. Lifetime must be explicit on provide:

Form Meaning
provide scoped IStore as SqlStore Type mapping
provide transient ReportProcessor Self-registration
provide singleton IClock as new Clock(zone) Construction recipe evaluated according to lifetime
provide singleton ILogger with key "audit" as AuditLogger Keyed registration
provide another transient IValidator <of Customer> as AddressValidator Add a registration without replacing others

as new ... is a recipe, not an already-created instance. Existing instances and unusual factories use the registry API. Normal provide establishes/replaces the effective registration for that exact identity, including its implementation/recipe and lifetime. It does not blindly append every declaration.

CS-SERVICE-009. needs all T accepts zero or more contributors; an empty collection is valid. provide another is additive. provide all lifetime Contract discovers and appends eligible concrete implementations in the assembly associated with the composition declaration. Open-generic discovery is supported; candidates register against the actual contracts they implement. An optional where uses ordinary Boolean/type-metadata expressions. Discovery occurs at composition execution; reflection is not a mandatory backend implementation.

20.5 Lifetimes and graph safety

CS-SERVICE-010. A singleton has one stable instance per root service environment/provider. A scoped service has one instance per active scope. A transient has a new instance per activation/resolution. Registration lifetime is not a class modifier. Singleton is not a static/global property, and eager versus lazy singleton construction is not guaranteed.

CS-SERVICE-011. A singleton dependency graph must not reach a scoped service, including through a transient or collection contributor. A transient that reaches a scoped dependency is effectively scope-bound. Within a scope, repeated resolution of the same scoped registration returns the same instance; distinct/nested scopes have independent scoped instances.

Where registrations and needs are visible, diagnose provable missing dependencies, key mismatches, cycles, invalid recipes and lifetime capture, showing the dependency path. Opaque registry/native registration can prevent complete static graph analysis; runtime container validation remains necessary for that part.

20.6 Providers and scopes

CS-SERVICE-012. Standard runtime abstractions are ServiceProvider, ServiceScope, ServiceScopeFactory, ServiceRegistry and MainServiceProvider. A services module supports creating an independent provider or registering into an existing native/host registry. Declaring the module does not create an ambient provider.

public action WriteDailyReport(outputDirectory as String)
    using provider = ReportingServices.Create(outputDirectory)
        using scope = provider.CreateScope()
            let writer = scope.GetService <of FileReportWriter>()
            writer.Write("daily.txt", "Daily import completed")
        end using
    end using
end action

A host that already owns DI registers the module into the host registry rather than building a second provider.

CS-SERVICE-013. Retrieval is explicit:

Operation Contract
scope.GetService <of T>() Required singular service; fail clearly when absent
scope.GetAllServices <of T>() Zero or more services; empty collection when absent
scope.HasService <of T>() Registration availability; avoid activation for inspection
provider.CreateScope() Create an ordinary bounded, disposable scope
scopeFactory.Create() Create a scope through the narrow scope-factory capability
.Native Explicit native-container interop

Keys are honored exactly. Retrieved services are borrowed/provider-owned. Root-provider direct retrieval is restricted to singleton/root-safe services; scoped and transient provider-owned services are obtained through an explicit scope. HasService may report a scoped registration at root even though root retrieval is forbidden. Availability is not activation permission. There is no ambient current scope.

CS-SERVICE-016. Required singular lookup of an unregistered identity throws DirectSharp.ServiceNotFoundException. It does not return nothing or fabricate a service. This exception denotes missing registration, not an arbitrary constructor/activation failure, disposed provider or root scope-bound retrieval violation. Preserve those other failures under their own contracts instead of relabeling them as missing registration. GetAllServices remains present empty for no matches and HasService remains registration inspection. Direct native Microsoft DI calls retain their own contracts.

CS-SERVICE-017. Ordinary generic lookup uses GetService<T>(), GetAllServices<T>() and HasService<T>(), with keyed overloads of those same names taking object key. Their CLR result types are T, IReadOnlyList<T> and Boolean respectively. Keys preserve exact typed identity, including case-sensitive String identity; this introduces neither GetKeyedService names nor new language grammar. The library declaration catalogue records the six members. CS-SERVICE-019 separately defines null-key handling.

CS-SERVICE-018. GetAllServices resolves and collects all matching services at the call and returns IReadOnlyList<T>, present empty for no matches. Reusing that result reuses the resolved instances and does not activate additional transient instances through enumeration. A separate call still follows each registration's lifetime. Activation failure occurs at the call with no successful partial result; already-created values remain provider/scope-owned, with no rollback or ownership transfer implied. A read-only list neither makes its service objects immutable nor extends their scope lifetime or grants disposal authority. Existing root-safety and exact registration matching remain applicable; this rule does not select additional registration-order behavior.

CS-SERVICE-019. Keyed GetService, GetAllServices and HasService reject a nothing/null key with ArgumentNullException before lookup or activation. Use a no-key overload for unkeyed identity. A statically known invalid service-key declaration is diagnosed rather than silently changed into unkeyed lookup. This selects no new key grammar and does not alter ordinary maps or exact typed/case-sensitive key equality.

CS-SERVICE-020. DirectSharp.ServiceNotFoundException derives from InvalidOperationException. Its read-only ServiceType property has CLR Type type and identifies the requested contract; its read-only ServiceKey property has nullable Object type and retains the exact key, or null for unkeyed lookup. The dedicated class retains CS-SERVICE-016's missing-registration-only meaning. Clients can inspect identity without parsing Message or catch the broader InvalidOperationException category. Constructor rules are in CS-SERVICE-021; do not dump unrelated runtime values into diagnostics.

20.7 Main provider and assisted construction

CS-SERVICE-014. MainServiceProvider.Set(provider) appoints an already-created provider without taking ownership. At most one main provider is active. Setting another while one is active fails explicitly. No main provider is legal; using main-provider functionality without one fails with a clear infrastructure error. A bounded registration token can clear the reference without disposing the provider.

using provider = ReportingServices.Create(outputDirectory)
    using main = MainServiceProvider.Set(provider)
        using scope = MainServiceProvider.CreateScope()
            let writer = scope.GetService <of FileReportWriter>()
            writer.Write("startup.txt", "Reporting is ready")
        end using
    end using
end using

The fragment belongs in an action with outputDirectory. Libraries do not appoint the application's main provider merely by being referenced.

CS-SERVICE-015. new T(inputs) from scope constructs a fresh caller-owned object and obtains missing DirectSharp needs from that explicit scope. Explicit with need inputs override provider-supplied inputs. It does not resolve T's own registration or adopt that registration's lifetime. Injected needs remain borrowed. Root-assisted construction is legal only when all supplied needs are root-safe; otherwise a scope is required.

20.8 Reference C# relation

A need can lower to ordinary constructor parameters and stored members with generated validation:

public class FileReportWriter
{
    private readonly string outputDirectory;

    public FileReportWriter(string outputDirectory)
    {
        this.outputDirectory = outputDirectory
            ?? throw new ArgumentNullException(nameof(outputDirectory));
    }

    public void Write(string name, string contents)
    {
        string path = Path.Combine(outputDirectory, name);
        File.WriteAllText(path, contents);
    }
}

The readonly field illustrates stable dependency storage. Service registration can be represented through standard Microsoft DI descriptors. Normal DirectSharp provide replacement needs identity-aware replacement; repeatedly calling an additive registration API would not preserve its semantics. Provider adaptation also preserves root restrictions, borrowed ownership, exact keys and scope lifetimes; emitting constructor parameters alone is insufficient.

20.9 Missing-service exception construction

CS-SERVICE-021. DirectSharp.ServiceNotFoundException has constructors (Type serviceType, Object serviceKey = nothing) and (Type serviceType, Object serviceKey, String message, Exception innerException), with nullable CLR key/message/inner parameters as shown in the API catalogue. A null serviceType throws ArgumentNullException. A null key records unkeyed identity and is distinct from passing null to keyed lookup. Store the identity without normalization. A null message chooses a useful default without an exact English-message parsing contract; preserve the supplied inner cause. Do not supply parameterless/message-only constructors without service identity, or a formatter-based serialization constructor/contract. CS-SERVICE-016/020's missing-required-registration-only meaning, base and read-only identity properties remain unchanged.


Specification contents · Previous: 19. Events · Next: 21. Attributes and documentation metadata