On this page

Dependencies, scopes and events

Application objects often depend on a clock, a repository or a journal. Explicit dependencies make those relationships visible. A composition root chooses implementations and lifetimes. Events describe notifications emitted by objects without requiring their consumers to be part of the same service contract.

The examples below use an import journal: each import obtains a scoped journal, records timestamps and reports progress to a listener. Storage is deliberately in memory so the lifetime behavior is visible without database setup.

needs: declare dependencies

public interface IClock
    action Now()
        returns Global.System.DateTimeOffset
    end action
end interface

public class UtcClock
    implements IClock

    public action Now()
        returns Global.System.DateTimeOffset
        implements IClock.Now

        return Global.System.DateTimeOffset.UtcNow
    end action
end class

public class ImportJournal
    needs
        clock as IClock
    end needs

    private entries as new Global.System.Collections.Generic.List <of String>

    public action Record(message as String)
        entries.Add("{{clock.Now():O}} {{message}}")
    end action

    public action Snapshot()
        returns Global.System.Collections.Generic.List <of String>
        return new Global.System.Collections.Generic.List <of String>(entries)
    end action
end class

The journal cannot be successfully constructed without a clock. The need is stable and private by default. It is part of the object contract across all constructors, rather than a parameter that happens to appear in one overload.

Direct construction is ordinary DirectSharp:

let journal = new ImportJournal() with
    clock: new UtcClock()
end with
journal.Record("Customer import started")

This fragment belongs in an application action. Supplying a need does not transfer ownership of that dependency to the journal. A provider-created dependency remains owned by its provider. If an object owns a disposable resource, represent that ownership separately from needs.

An optional need has a default, commonly = nothing. Keep optionality intentional: it should mean the object has useful behavior without that dependency. A required need must be supplied and cannot be absent when its type permits absence.

services: register dependencies and lifetimes

services ImportServices
    provide singleton IClock as UtcClock
    provide scoped ImportJournal
    provide transient CustomerImporter
end services

A singleton clock has stable identity within this provider. The journal is shared by resolutions within one import scope. A CustomerImporter is new for each activation.

public class CustomerImporter
    needs
        journal as ImportJournal
    end needs

    public event ProgressChanged(completed as Integer, total as Integer)

    public action Import(names as Global.System.Collections.Generic.IReadOnlyList <of String>)
        journal.Record("Starting {{names.Count}} customers")
        raise_event ProgressChanged(0, names.Count)

        let completed = 0
        for_each name in names
            require name is not whitespace
            journal.Record("Accepted customer {{name}}")
            completed += 1
            raise_event ProgressChanged(completed, names.Count)
        end for_each

        journal.Record("Import completed")
    end action
end class

The importer borrows the journal and does not dispose it. Its event has an ordinary typed signature; no special EventArgs object is necessary.

using / services: own provider and service scopes

public action RunImport(names as Global.System.Collections.Generic.IReadOnlyList <of String>)
    action DisplayProgress(completed as Integer, total as Integer)
        Global.System.Console.WriteLine("{{completed}} of {{total}} customers")
    end action

    using provider = ImportServices.Create()
        using scope = provider.CreateScope()
            let importer = scope.GetService <of CustomerImporter>()
            let journal = scope.GetService <of ImportJournal>()

            event importer.ProgressChanged add_handler DisplayProgress
            try
                importer.Import(names)
            finally
                event importer.ProgressChanged remove_handler DisplayProgress
            end try

            for_each entry in journal.Snapshot()
                Global.System.Console.WriteLine(entry)
            end for_each
        end using
    end using
end action

The provider and scope are caller-owned disposable objects. The retrieved importer and journal are borrowed. The scope owns the services it creates and releases them according to the provider's lifetime contract. Putting the retrieved journal into a separate ownership-taking using would violate that contract.

GetService <of T>() means a required service: an unavailable registration fails immediately with a useful error. GetAllServices <of T>() returns zero or more services. HasService <of T>() asks whether a registration is available without activating it where the provider supports introspection.

Root retrieval is restricted to singleton/root-safe services. A scoped service requires an explicit scope. A transient that depends on the scoped journal also belongs in a scope. Making CustomerImporter a singleton would create a singleton-to-scoped dependency path and is invalid.

Nested scopes are independent. They do not reuse a parent's scoped journal. A scope should describe an actual unit of work, such as one request, import or message, rather than become a permanent global object.

provide / provide another: replace or collect registrations

Normal provide replaces the effective registration for the exact identity. This is useful when application composition overrides a framework or module default.

Use provide another when several implementations should coexist. A consumer declares needs all T for such a collection; no contributors means an empty collection rather than a missing-dependency failure.

A service key is an exact identity value. String keys "audit" and "Audit" are different, even though source identifiers are case-insensitive. This matters when matching keyed registrations across library boundaries. DirectSharp does not normalize those keys.

For advanced registrations, use the native/registry API. An ASP.NET Core or Generic Host application already has a service registry and owns its provider. Register a services module into that registry rather than create a second provider with independent singleton instances.

event / add_handler / remove_handler: manage subscriptions

Imperative subscription uses event source.Event add_handler handler. Removal uses the same handler identity. The try/finally in RunImport protects cleanup if validation or a subscriber throws.

Events use normal .NET multicast delivery. Raising an event does not schedule work on a UI dispatcher or make handlers asynchronous. A handler exception can propagate from the raise. If an application needs isolation between subscribers, weak subscriptions or a message bus, use a library that explicitly supplies that policy.

Only the declaring type can raise its event. Consumers can subscribe and remove handlers, but cannot impersonate the producer by raising it themselves.

handles_event: follow a replaceable event source

When an object should remain attached to whichever source it currently stores, declarative handlers avoid repetitive rewiring.

public class ImportProgressDisplay
    needs
        replaceable importer as CustomerImporter
    end needs

    private action ShowProgress(completed as Integer, total as Integer)
        handles_event importer.ProgressChanged

        Global.System.Console.WriteLine("Imported {{completed}} of {{total}}")
    end action
end class

Changing the tracked importer unsubscribes the old source and subscribes the new one. An absent optional source has no subscription. The compiler manages the relationship for the display object's lifetime.

The source must be something the compiler can track, such as a field, property or need. A temporary call result is unsuitable: handles_event GetImporter().ProgressChanged cannot establish a deterministic relationship that rewires when stored state changes.

A replaceable need is an explicit design choice. It does not automatically resolve a new dependency from DI or dispose the previous one. Use it where replacing the source is meaningful, rather than treating all injected references as mutable.

The complete event reference follows a progress log across two source objects, ends the observation session explicitly and explains delegate identity, handler failures and direct imported .NET events.

Read the service contracts and event contracts for exact ownership, binding and diagnostic rules.

Previous: Async and concurrency · Next: Application design