Application lifecycle¶
Diana applications have an explicit lifecycle:
runDiana() drives all of it (via DianaRuntime.run); tests and embeddings
can drive each step themselves.
@PostConstruct / @PreDestroy¶
Run code after construction and during shutdown, Spring-style:
@Repository()
class Db {
late final Connection _conn;
@PostConstruct()
Future<void> connect() async => _conn = await Connection.open(...);
@PreDestroy()
Future<void> disconnect() => _conn.close();
}
Rules (build-verified): public instance method, no parameters, returns
void or Future<void>, at most one of each per class, singleton
providers only.
initialize()constructs every singleton in dependency order and awaits each@PostConstruct— including@ConfigurationPropertiesbinding, so missing required config keys fail here, before the port opens.dispose()runs@PreDestroyhooks in reverse dependency order, after the server has stopped and in-flight requests have drained.
Publishing application events from @PostConstruct is fully
supported — listeners are registered before initialization runs.
Graceful shutdown¶
runDiana() installs SIGINT/SIGTERM handlers (SIGTERM skipped on Windows).
On a signal it stops accepting connections, waits for in-flight requests,
then disposes. Programmatic control:
final shutdown = Completer<void>();
await DianaRuntime.run(
() async { final app = await buildDianaApp(); ... },
installSignalHandlers: false,
shutdownTrigger: shutdown.future,
);
The generated DianaApp¶
final app = await buildDianaApp(config: ..., overrides: ...);
app.context; // the DI container
app.server; // the DianaServer
await app.initialize();
await app.server.serve(port: 0);
...
await app.server.close();
await app.dispose();
When @Scheduled tasks exist, app.initialize() also starts the scheduler
and app.dispose() stops it; with multi-isolate serving, tasks run once
per process unless annotated perIsolate: true (see
Observability § Scheduling).
Test overrides — DianaOverrides¶
Every provider gets a nullable slot in the generated DianaOverrides
class; a non-null value replaces the real provider everywhere it is
injected (Spring @MockBean analog):
class FakeRepo implements InMemoryTodoRepository { ... }
final ctx = buildDianaContext(
overrides: DianaOverrides(inMemoryTodoRepository: FakeRepo()),
);
expect(ctx.todoService.repo, isA<FakeRepo>());
Notes:
- Overrides are per concrete provider class (unambiguous when several
classes implement one interface). Fakes can
implementsthe concrete class. - Works with
buildDianaContext,buildDianaServer,buildDianaAppandrunDiana. - Overriding a transient provider effectively makes it a singleton for that context — exactly what tests want.
- With multi-isolate serving, overrides must be sendable (tests normally run single-isolate; not a practical concern).