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.