A small “composition root + dependency manifest” helper built on Spring4D.
This unit gives you:
- A Composition Root base class (
TDICompositionRoot) to centralize container setup. - A dependency manifest / registry (
TDependencyRegistry) that collects “things the app needs” across units. - Fail-fast startup validation so missing registrations blow up at startup, not at 2am when a user clicks a forgotten button.
- A legacy-friendly
BuildUp()helper for injecting into objects not created by the container.
It’s designed for hybrid/legacy Delphi applications where not everything is DI-created, but you still want DI-level safety.
In old codebases you often have:
- classes instantiated manually (
TLegacyForm.Create,TMyThing.Create), - service locator usage in random places,
- and DI registration scattered / incomplete.
This unit lets you:
- Declare required services from anywhere (even legacy units),
- Build the container in one place,
- Validate the required services at startup,
- Optionally inject into existing objects with
BuildUp().
TValidationMode = (vmRegisteredOnly, vmResolveAtStartup, vmIgnore);-
vmRegisteredOnly
Service must be registered in the container (but not necessarily resolvable without runtime params). -
vmResolveAtStartup
Service must be resolvable during startup validation (Container.Resolve(...)succeeds).
Use this for “core services” that should always be constructible immediately. -
vmIgnore
Opt-out: do not register as a requirement and do not inject (for fields/properties).
Put this on:
- constructor parameters (for validation only),
- fields,
- properties.
constructor TSomeService.Create(
[DIRequire] Logger: ILogger;
[DIRequire(vmResolveAtStartup)] Repo: IUserRepository;
const TenantId: Integer // runtime param => not annotated
);In this unit’s conventions:
[DIRequire]means: “this dependency matters and should be validated”.- For fields/properties,
[DIRequire]also acts as an injection marker, same as[Inject].
Spring4D’s attribute for field/property injection.
If [Inject(ServiceType)] is used, the service type can be overridden.
If [Inject] is used with no service type, the member type is used.
Instead of hoping you remembered to register everything, you can build a manifest by calling:
TDependencyRegistry.Instance.RegisterRequirement(TypeInfo(IMySvc));- or scanning a class:
TDependencyRegistry.Instance.RegisterRequirementsFrom(TMyClass);
Then TDICompositionRoot.ValidateDependencies checks that the container satisfies the manifest.
Create your application-specific root class:
type
TAppRoot = class(TDICompositionRoot)
protected
procedure RegisterDependencies; override;
end;
procedure TAppRoot.RegisterDependencies;
begin
inherited;
// add modules here or direct container regs
// Container.RegisterType<TFoo>.Implements<IFoo>;
end;In any unit that needs a service (especially legacy code), declare the requirement in initialization:
initialization
TDependencyRegistry.Instance.RegisterRequirement(TypeInfo(ILogger));
// or:
TDependencyRegistry.Instance.RegisterRequirementsFrom(TMyLegacyClass);This keeps consumers decoupled from implementations and moves “missing DI wire” failures to startup.
In your .dpr (or app startup point):
var
Root: TAppRoot;
begin
Root := TAppRoot.Create;
try
Root.SetUp(True); // True = validate
Application.Initialize;
Application.Run;
finally
Root.Free;
end;
end;SetUp(True) does:
BeforeRegisterDependenciesRegisterDependenciesContainer.BuildValidateDependencies(optional)AfterRegisterDependencies
You can register dependencies in modules:
- Procedure-based modules:
Root.AddModule(
procedure(C: TContainer)
begin
C.RegisterType<TFoo>.Implements<IFoo>;
end
);- Class-based modules:
type
TFooModule = class(TDIRegistrationModule)
public
procedure RegisterDependencies(aContainer: TContainer); override;
end;
procedure TFooModule.RegisterDependencies(aContainer: TContainer);
begin
aContainer.RegisterType<TFoo>.Implements<IFoo>;
end;
Root.AddModule(TFooModule.Create);The root owns class-based modules and frees them in its destructor.
A field/property is considered a requirement if:
- it has
[DIRequire](unless mode isvmIgnore), or - it has
[Inject](unless[DIRequire(vmIgnore)]is also present).
Constructor parameters are requirements only if they have [DIRequire].
A field/property is injected if:
- it has
[DIRequire](unlessvmIgnore), or - it has
[Inject](unless[DIRequire(vmIgnore)]is also present).
Service type selection:
- If
[Inject(ServiceType)]provides a service type, it is used. - Otherwise, the member’s RTTI type is used.
Only types with RTTI kind tkInterface or tkClass are injected.
Overwrite behavior:
aOverwriteExisting=False(default): inject only if the current value is “unset”.aOverwriteExisting=True: inject even if something is already assigned.
If you create an object manually but still want Spring4D field/property injection:
var
Obj: TMyLegacyThing;
begin
Obj := TMyLegacyThing.Create;
GlobalContainer.BuildUp(Obj); // or Root.Container.BuildUp(...)
end;This is intended for:
- forms / frames created normally,
- legacy classes created with
Create, - objects that can’t be created by the container yet.
BuildUp() checks whether a property is “unset” by calling Prop.GetValue(...).
That means the property getter will run.
Best practice:
- Keep injected property getters side-effect-free (no lazy init that depends on injected values, no exceptions, no DB calls).
If a getter is “unsafe during startup”, prefer field injection for that dependency.
Use vmResolveAtStartup for:
- core app services,
- logging,
- configuration,
- DB connection factories,
- anything that should always be constructible immediately.
Use vmRegisteredOnly for:
- services that require runtime parameters,
- services that depend on environment state not ready at startup.
Because BuildUp() can inject class instances (tkClass), you must be clear about ownership:
- If the container owns the instance (singleton/transient controlled by Spring4D),
do not
Freeit manually unless you explicitly created/owned it. - Prefer injecting interfaces where possible.
This unit allows [DIRequire] to imply injection for fields/properties.
That’s convenient, but it also means someone adding [DIRequire] can change runtime behavior.
Recommended team rule:
- Add
[DIRequire]only when you genuinely want both validation and injection for members.
Use [DIRequire(vmIgnore)] to explicitly block injection/validation for a specific member.
If Spring4D “doesn’t call your constructors” or RTTI scanning is incomplete, ensure RTTI is enabled, e.g.:
{$RTTI EXPLICIT METHODS([vcPublic, vcPublished]) PROPERTIES([vcPublic, vcPublished])}In DEBUG builds the unit hooks InitProc so if requirements were registered but never validated, it raises an error early.
This catches “we forgot to call SetUp(True) / ValidateDependencies” in debug testing.
- For constructor params: use
[DIRequire]if you want them validated. - For fields/properties: either works.
[DIRequire]implies injection in this design.
[Inject]is classic Spring4D injection.
Yes:
TDependencyRegistry.Instance.RegisterRequirementsFrom(TMyType);This picks up [Inject]/[DIRequire] on fields/properties and [DIRequire] on constructor params.
type
TFoo = class
private
[DIRequire(vmResolveAtStartup)]
FLog: ILogger;
[Inject]
property Repo: IUserRepository read FRepo write FRepo;
end;
initialization
TDependencyRegistry.Instance.RegisterRequirementsFrom(TFoo);Startup:
Root := TAppRoot.Create;
Root.SetUp(True);Legacy object:
Obj := TFoo.Create;
GlobalContainer.BuildUp(Obj);Internal MaxLogic helper unit. Use freely within the codebase.