Guide Home | Previous: 3. Core Concepts | Next: 19. Configuration
18. DI Container¶
The document that owns this chapter's contract — covered by C++ configuration and host public contract. This chapter explains registration and resolution for the C++-only built-in DI container.
The framework has a built-in ASP.NET Core-style DI (dependency injection) container.
Register services with service_collection_t, and pull them out with service_provider_t.
A handler only has to take dependencies as constructor parameters and the framework
auto-injects them from the current scope.
1. Three Lifetimes¶
| Lifetime | Registration | Instance count |
|---|---|---|
| singleton | add_singleton<T>() |
1 for the whole app |
| scoped | add_scoped<T>() |
1 per execution context (scope) |
| transient | add_transient<T>() |
A new instance on every resolve |
service_scope_kind_t is an enum that distinguishes the purpose of a scope the Framework
creates. The application never creates a scope directly. The Framework creates and cleans
up the right scope at handler-dispatch, STREAM-session, and Spot-activation boundaries. An
Actor payload is processed as a member function of its containing Spot, so there's no
separate Actor handler registration surface or public handler class.
| Scope kind | Lifetime scope |
|---|---|
handler_invocation |
The handler call that processes one HTTP request |
stream_session |
One stream connection's lifetime |
spot_activation |
Spot activation |
entry_spot |
Entry Spot |
actor_creation |
Actor creation |
2. How To Register¶
Registering With A Default Constructor¶
// A type that can be constructed with T()
options.services ().add_singleton<season_store_t> ();
options.services ().add_transient<request_counter_t> ();
Registering With Dependency Injection — The Container Resolves Constructor Arguments¶
// add_singleton<T, Dep1, Dep2, ...>() -- requires a constructor T(Dep1&, Dep2&, ...)
options.services ()
.add_singleton<bingo_room_allocator_t> ()
.add_singleton<agent_availability_directory_t> ()
.add_singleton<agent_assignment_service_t,
bingo_room_allocator_t,
agent_availability_directory_t> ();
// ^ the constructor receives both deps by reference
Constructor-signature rule: dependencies must be received as T& references. No
values, no pointers.
class agent_assignment_service_t
{
public:
// The container calls this constructor
agent_assignment_service_t (bingo_room_allocator_t &allocator,
agent_availability_directory_t &availability)
: _allocator (allocator), _availability (availability) {}
};
Registering A Pre-Built Instance¶
auto topology = std::make_unique<sample_topology_t> (config);
options.services ().add_singleton<sample_topology_t> (std::move (topology));
Use this for objects that need external initialization -- a topology built by parsing config, an external client that needs a connection string.
Registering With A Factory Lambda¶
options.services ().add_factory<http_client_t> (
[] (zlink::framework::service_provider_t &provider) {
auto &config = provider.get_required<connection_config_t> ();
return std::make_unique<http_client_t> (config.base_url, config.timeout);
},
zlink::framework::service_lifetime_t::singleton);
Use this for complex initialization logic that's hard to express through constructor injection.
3. Automatic Handler Injection¶
A channel/HTTP handler receives constructor injection in the dispatch scope when its
constructor takes dependencies. There's no need to separately call add_transient<T>().
Spot packet and Actor payload handlers are Spot member functions, so they aren't registered
as DI handler classes. A timer handler, being a separate class, is created once per Spot
activation and reused across timer ticks in that same activation. Its constructor parameters
are also resolved within the Spot activation scope.
class create_game_http_handler_t
{
public:
using request_type = create_game_http_req_t;
using reply_type = create_game_http_res_t;
static constexpr const char *topic_name = "CreateGame";
// The Framework resolves these constructor parameters in this dispatch scope.
explicit create_game_http_handler_t (
zlink::framework::request_client_t &client,
zlink::framework::logger_t<create_game_http_handler_t> &logger)
: _client (client), _logger (logger) {}
create_game_http_res_t handle (const create_game_http_req_t &request);
private:
zlink::framework::request_client_t &_client;
zlink::framework::logger_t<create_game_http_handler_t> _logger;
};
Because a handler is created as a new instance per request (transient), use an injected service reference only within the handler's own lifetime.
4. Built-In Framework Services¶
Services the framework registers when the app runs, or provides while it constructs a handler. Take the type you need as a handler constructor parameter to receive it through constructor injection.
| Service | Description |
|---|---|
request_client_t |
Sends a channel request — request(mesh, channel, msg).async<TReply>() |
logger_t<TOwner> |
A logger tagged with the owning type's name — _logger.info(...) |
session_actor_manager_t |
Creates, looks up, and binds Actors from a stream session |
logger_factory_t |
create("category") — for when the category is decided dynamically (create<TCategory>() is type-name-based) |
logger_t<T> auto-tags the log source name with T's type name — so even an app with
many handlers can tell log sources apart.
5. Pulling Directly From hosted_service_t¶
An app-lifecycle service (hosted_service_t) pulls directly from the container at
start(service_provider_t &services). The service it needs must already be registered.
class season_scheduler_t : public zlink::framework::hosted_service_t
{
public:
void start (zlink::framework::service_provider_t &services) override
{
// Pulled from the container at start -- null before this
_store = &services.get_required<season_store_t> ();
_worker = std::thread ([this] { run_schedule (); });
}
void stop () noexcept override
{
_running = false;
if (_worker.joinable ()) _worker.join ();
}
private:
season_store_t *_store = nullptr;
std::atomic<bool> _running{true};
std::thread _worker;
};
6. Lifetime Selection Guide¶
| Situation | Recommended lifetime |
|---|---|
| A pure config or read-only object with no shared state (topology, config) | singleton |
| Infrastructure reused across the whole app, like a connection or client | singleton + an internal thread-safe implementation |
| State that must be isolated per request (transaction, per-request context) | scoped |
| A channel/HTTP handler — needs a new instance per request | transient (auto-registered from its constructor) |
| Mutable domain state (a game room, a conversation's state) | Not DI — managed by SPOT (Chapter 6) |
7. Watch For Lifetime Mismatches — Captive Dependency¶
A singleton must not be injected with a scoped/transient service. A singleton lives for the whole app, so referencing a shorter-lived service can leave a scoped object referenced after its scope ends, and a transient object's intent -- creating a fresh one per request -- gets captured by the singleton and disappears.
// Wrong -- a singleton references a transient
options.services ()
.add_transient<conversation_context_t> ()
.add_singleton<support_service_t, conversation_context_t> ();
// ^ the singleton "captures" the transient
// it ends up holding onto the reference to whichever conversation_context_t it was
// first injected with, forever
// Correct
options.services ()
.add_singleton<conversation_context_t> () // If it's safe to share, make it a singleton
.add_singleton<support_service_t, conversation_context_t> ();
Rule: the lifetime of the service you're registering must not exceed the lifetime of the dependency it injects.
8. Concurrent Access To A Singleton Service¶
A singleton is accessed concurrently by multiple threads in the worker pool. If it has mutable state, it absolutely needs its own synchronization.
class agent_availability_directory_t
{
public:
void set_available (const std::string &agent_id, bool available)
{
std::lock_guard lock (_mutex); // Protects concurrent writes
_availability[agent_id] = available;
}
bool is_available (const std::string &agent_id) const
{
std::shared_lock lock (_mutex); // Allows concurrent reads
auto it = _availability.find (agent_id);
return it != _availability.end () && it->second;
}
private:
mutable std::shared_mutex _mutex;
std::unordered_map<std::string, bool> _availability;
};
A read-only singleton (topology, a config struct) is safe with no lock at all if it exposes
only const methods.
9. Related Documents¶
- The formal contract: C++ configuration and host public contract
- Reading config values: 19. Configuration
- The list of injectable types: 13. Key Type Usage Index