# DependencyInjection
🌍 Language: English | Español
IoC/DI container for Delphi with a fluent API inspired by .NET’s Microsoft.Extensions.DependencyInjection. Register services once, resolve them anywhere — with Singleton, Scoped, and Transient lifetimes.
Why use this?
- 🔌 Familiar API — Same
AddSingleton/AddTransient/AddScopedpattern as .NET - 🎯 Generic & type-safe — Compile-time verification via generics, no string tokens
- ⚡ Three lifetime modes — Singleton, Scoped (per scope), Transient (new every time)
- 🏭 Flexible registration — By class, by factory lambda, or by existing instance
- 💤 Lazy resolution —
ILazy<T>in the consumer’s constructor breaks a cycle between two services; the registration is untouched andValueasks the container on every use - 🦆 Duck Typing —
TDuckDecorator<T>wraps any object implementing the same interface shape - 📦 Zero external deps —
DependencyInjection.AbstractionsandCommons, both DAF’s own
Quick Start
uses
Daf.Extensions.DependencyInjection,
Daf.DependencyInjection;
var
Services: IServiceCollection;
Provider: IServiceProvider;
begin
Services := TServiceCollection.Create;
// Register
Services.AddSingleton<IMyService, TMyService>;
// Build the container (freezes the collection)
Provider := Services.BuildServiceProvider;
// Resolve
var Svc := Provider.GetRequiredService<IMyService>;
Svc.DoWork;
// Always shut down when done
Provider.ShutDown;
end;Registration overloads
// By implementation class
Services.AddSingleton<IMyService, TMyServiceImpl>;
// By factory lambda (full control)
Services.AddTransient<IMyService>(
function(P: IServiceProvider): IMyService
begin
Result := TMyServiceImpl.Create(P.GetRequiredService<IConfig>);
end);
// By existing instance (Singleton only)
Services.AddSingleton<IMyService>(MyExistingInstance);Keyed services
Two implementations of the same contract, told apart by a key. Same names as Microsoft.Extensions.DependencyInjection: AddKeyedSingleton, GetRequiredKeyedService, RemoveAllKeyed.
Services.AddKeyedSingleton<IStorage, TBlobStorage>(TValue.From<TStorageKind>(Blob));
Services.AddKeyedSingleton<IStorage, TFileStorage>(TValue.From<TStorageKind>(FileSystem));
var Blobs := Provider.GetRequiredKeyedService<IStorage>(TValue.From<TStorageKind>(Blob));
var Missing := Provider.GetKeyedService<IStorage>('nobody'); // nilAddKeyedSingleton, AddKeyedScoped and AddKeyedTransient come in the same forms as their Add* — by class, by factory and by instance — and each has its TryAddKeyed*.
An enumerated key is the recommended form: renaming a member reaches every use, and a typo is a compile error. A string key such as 'bolb' compiles.
A key can be a string, an integer, a boolean, a value of an enumerated type, a metaclass or a PTypeInfo. Two keys are the same when their type and value match, so two enumerated values of different types that share an ordinal are different keys, and the string '42' is not the integer 42. Anything else — a record, an object — is rejected where it enters, naming the type: the container cannot define an equality that those types do not have.
⚠️ A character is not a key. A one-character key is a string key: use a string.
The two spaces are isolated
A registration made with a key is not reachable without one, and the other way round. A contract with a key is a different contract:
GetServices<T>does not include the keyed registrations, andCanResolvewithout a key answers only for the keyless ones.ReplaceandRemoveAllreach only the keyless ones.RemoveAllKeyedtakes the key.TryAdd*asks about the pair, not the type: one key does not block another, and neither blocks the keyless registration.Decoratewraps the keyless registrations and leaves the keyed ones alone. Decorating a type that has only keyed registrations fails saying nothing is registered for it.
Asking for a key from a constructor or a property
The same mark serves both positions, and the key travels inside it:
type
TReportService = class(TInterfacedObject, IReportService)
private
FStorage: IStorage;
public
constructor Create([DafInject('audit')] const Audit: IAuditLog);
[StorageInject(FileSystem)]
property Storage: IStorage read FStorage write FStorage;
end;[DafInject] takes a string, an integer, a boolean, a metaclass or a TypeInfo(...). For an enumerated key the application declares its own mark, in one line:
type
StorageInjectAttribute = class(DafInjectAttribute<TStorageKind>);The container recognises it by descent, so it never needs to know that enumeration.
⚠️ That mark goes in a unit of its own, separate from the classes that use it. Declared in the same unit, reading the attribute aborts with an access violation.
⚠️ And the generic is never written inside a [ ]. [DafInject<TStorageKind>(...)] compiles, with a W1074 Unknown custom attribute almost nobody reads, and at run time the property has no attribute at all: the injection does not happen and nothing says so.
For a one-off enumerated key without declaring a mark there is an escape, [DafInject(TypeInfo(TStorageKind), Ord(FileSystem))]. The ordinal is checked against the range of the type, and one that does not belong fails naming the property.
One registration for any key
A registration under ServiceKey.Any serves every key of its service type that has no registration of its own, and its factory is told which one was asked for:
Services.AddKeyedSingleton<IStorage>(ServiceKey.Any,
function(P: IServiceProvider; const Key: TValue): IStorage
begin
Result := TStorage.Create(Key.ToString);
end);A concrete registration beats it — the wildcard is asked only when the direct lookup fails — and each key it serves gets its own instance. The sentinel is a value of a type declared inside the DI module, and it cannot be built from outside.
⚠️ A keyed registration cannot leave its generic arguments unbound. The two matchings are not combined, and the registration is rejected on the spot.
Property injection
The constructor is the default, and this does not replace it. Two cases need property injection:
- Two services that need each other. A cycle by constructor is unbuildable — neither constructor can run before the other finishes. By property it resolves.
- An instance the container does not build. A form is built by the platform, so there is no constructor for the container to choose.
Each property declares itself, and a class takes part by having one — there is nothing else to declare:
type
TReportService = class(TInterfacedObject, IReportService)
private
FStorage: IStorage;
public
[DafInject]
property Storage: IStorage read FStorage write FStorage;
end;The property must be of an interface type and writable; whether a field or a method backs it makes no difference, and neither does that field being strict private. A marked property that cannot be filled aborts naming the property, its type and the class.
The constructor does not see them. Injection happens once the constructor has returned, so a marked property is nil inside it. A dependency needed during construction goes through the constructor; both routes live together in the same class.
Only what the container builds from a class is wired. What a factory returns, and an instance registered as such, belong to whoever built them: the container fills no property of theirs, runs no code wiring on them and does not empty their properties when it closes. Their owner wires them on request with InjectInto. When a service registered by class is decorated, the original reaches the decorator already wired.
Wiring an instance the container did not build
TActivatorUtilities.InjectInto(Provider, Self); // in the form's constructorSame marks, same requirements, same errors as a resolution. The container does not take ownership of what it wires: it does not count it, closing does not destroy it, and the services it holds live as long as it holds them.
Wiring by code, with nothing marked
Services.AddInjector<TReportService>(
procedure(const Instance: TReportService; const Provider: IServiceProvider)
begin
Instance.Storage := Provider.GetRequiredService<IStorage>;
end);For what cannot be annotated: a class from another library, a unit whose property RTTI is trimmed — this route uses no RTTI at all, so it is the only one immune — and a service key that is awkward to write inside an attribute. It is a policy of the type, so it reaches descendants too, and several wirings run in the order they were registered. It runs after the constructor and after the attribute injection, and only on what the container builds.
⚠️ A trimmed property RTTI switches this off, and nothing says so. {$RTTI EXPLICIT PROPERTIES([])} leaves the properties of a unit out of the RTTI, and then a marked one does not exist for the sweep: the class looks like any other that marks nothing, so it resolves with its dependency left nil and the failure appears at the first use. The container cannot detect it. The directive is written by hand at the top of that same unit, and the way out is wiring by code, which uses no RTTI.
Scopes
var Scope := Provider.CreateScope;
try
var Svc := Scope.ServiceProvider.GetRequiredService<IScopedService>;
// Scoped instances are unique within this scope
finally
Scope := nil; // triggers Shutdown + managed cleanup
end;Threads
Once the provider is built, resolving from several threads at once is safe. Each registration is built once, on one thread, and every thread that asks gets that same instance — so a constructor does not have to be thread-safe.
What that guarantee covers is the container, not what comes out of it: a resolved service is as thread-safe as its author made it.
Composing is single-threaded. Register on one thread, build the provider, and from then on the collection is only read — registering after building fails saying it is frozen. There is no guarantee against composing from two threads.
A scope is a lifetime boundary, not a thread. A scope can be used from any thread, and from several at the same time, whoever created it. One scope per unit of work is still the recommendation, but the reason is the services, not the container: a service with mutable state does not become concurrent by living in a scope. And a scope per thread does not isolate singletons — the singletons scope is one for all scopes, so two threads with their own scopes still share it.
Closing is safe against resolutions in flight. A ShutDown concurrent with a resolution ends in one of two ways: the resolution completes and delivers its service, or it fails with EContainerShutDownError. If it had already built its instance when it lost the race, that instance is destroyed rather than handed out.
⚠️ A constructor that resolves on another thread and waits for the result deadlocks. The construction of one registration is serialised, so the thread that waits is holding what the other one needs. No container prevents this; resolve the dependency through the constructor instead.
AppModule pattern
Organise registrations by module:
type
TMyModule = class(TAppModule)
public
procedure AddServices(const Services: IServiceCollection); override;
end;
procedure TMyModule.AddServices(const Services: IServiceCollection);
begin
Services.AddSingleton<IMyService, TMyService>;
Services.AddTransient<IOther, TOtherImpl>;
end;Applied by name, in the order written — AddModule<T> instantiates the module and calls its AddServices on the spot, so nothing calls AddServices by hand:
Services
.AddModule<TMyModule>
.AddModule<TOtherModule>;See DependencyInjection.Abstractions for the IAppModule interface and all core contracts.
Documentation
- 📖 Usage Guide — lifetimes, scopes, Lazy, DuckDecorator, integration with Hosting
- 📖 Startup composition — modules and
AddXxxfunctions,TryAdd/Replace/Decorate,IStartupTask, graph validation - 📁 ModuleComposition — three modules applied in order, and the one that only the instance overload can carry
Abstractions
DependencyInjection.Abstractions
🌍 Language: English | Español
Core contracts (interfaces and types) for the DAF Dependency Injection system. This module contains no implementation — it defines the shape that consumers depend on and that DependencyInjection implements.
What’s in this module
| Unit | Contents |
|---|---|
Daf.Extensions.DependencyInjection |
IServiceCollection, IServiceProvider, IServiceScope, IAppModule, lifetimes, Factory, TDuckDecorator<T>, ILazy<T> y Lazy |
Daf.Extensions.Options |
IOptions<T>, IConfigureOptions<T>, IValidateOptions<T>, TValidateOptionsResult, TOptionsValidator<T>, EOptionsValidationError — the options pattern contract |
Daf.Options |
TOptions<T>, TConfigureOptions<T>, TValidateOptions<T> and AppOptions — settings registered with code |
Daf.AppModule |
Compatibility alias for IAppModule: the contract moved next to AddModule<T>, which is a method of the IServiceCollection record and cannot depend on this unit. An existing uses Daf.AppModule keeps working |
Daf.DependencyInjection.ActivatorUtilities |
TActivatorUtilities: build an object with its constructor arguments resolved, and InjectInto to wire an instance the container did not build |
Daf.DependencyInjection.OpenGenerics |
Matching, checking and activation for registrations whose type arguments are left open |
Key types
IServiceCollection
Fluent builder for registering services before the container is built.
Services
.AddSingleton<ICache, TMemoryCache>
.AddTransient<IRepo, TRepoImpl>
.AddScoped<IUoW, TDbUoW>;
var Provider := Services.BuildServiceProvider;IServiceProvider
The resolved container. Use it to obtain service instances.
var Svc := Provider.GetRequiredService<IMyService>;
var Opt: IMyService;
if Provider.TryGet<IMyService>(Opt) then ...
var All := Provider.GetServices<IPlugin>;TServiceLifeTime
type TServiceLifeTime = (Singleton, Scoped, Transient);IServiceScope
A lifetime boundary. Scoped services are shared within a scope and released when the scope ends.
var Scope := Provider.CreateScope;
var Svc := Scope.ServiceProvider.GetRequiredService<IScopedService>;
Scope := nil; // releases scoped instancesIAppModule
Convention for grouping service registrations:
type
IAppModule = interface(IInvokable)
procedure AddServices(const ServiceCollection: IServiceCollection);
end;ServiceKey and TServiceSlot
ServiceKey is everything the container knows about a service key: whether a TValue can be one, how two are compared, how one is named in a message, and which one is the wildcard.
ServiceKey.IsValid(Key); // a string, an integer, a boolean, an enumerated value,
// a metaclass or a PTypeInfo -- and the empty one, which means "no key"
ServiceKey.Check(Key); // aborts naming the type when it cannot be one
ServiceKey.Text(Key); // for a message, and for nothing else
ServiceKey.Any; // the wildcard keyTwo keys are the same when their type and value match, so two enumerated values of different types that share an ordinal are different keys. The rejection happens where the key enters — at registration and at resolution — and never when comparing two keys.
TServiceSlot is the pair (service type, key) the collection indexes by. An empty key is the keyless space, not a third state.
TServiceKeyedFactory<T>
TServiceKeyedFactory<ServiceType: IInterface> =
reference to function(Provider: IServiceProvider; const Key: TValue): ServiceType;A factory told which key is being asked for. It is what makes a wildcard registration useful: the only thing a factory serving every key can do is decide by which one it got.
DafInject
The mark of property injection: it goes on each property the container fills, and a class takes part by having one. There is nothing to declare on the class.
TReportService = class(TInterfacedObject, IReportService)
private
FStorage: IStorage;
public
[DafInject]
property Storage: IStorage read FStorage write FStorage;
end;The property must be of an interface type and writable. A marked property that cannot be filled raises EPropertyInjectionError naming the property, its type and the class.
One mark and not two. Under a trimmed RTTI the injection is off and nothing says so.
The key travels inside this mark, and the same mark serves a constructor parameter:
constructor Create([DafInject('audit')] const Audit: IAuditLog);[DafInject] accepts a string, an integer, a boolean, a metaclass, a TypeInfo(...), or a TypeInfo(...) plus an ordinal. It cannot accept a TValue: an attribute argument has to be a constant, and a TValue there gives E2026 even for a plain string literal — the TValue is built inside the constructor, which the RTTI calls when reading the attribute.
For an enumerated key the application declares its own mark over the generic base, in one line:
type
StorageInjectAttribute = class(DafInjectAttribute<TStorageKind>);The sweep recognises it by descent (Attr is DafInjectAttribute), so the framework never needs to know that enumeration. ⚠️ That mark goes in a unit of its own, separate from the classes that use it: declared in the same unit, reading the attribute aborts with an access violation. ⚠️ And the generic is never written inside a [ ] — [DafInject<TStorageKind>(...)] compiles with a W1074 and at run time the property has no attribute at all.
TServiceInjector<T> and AddInjector<T>
Wiring by code, for what cannot be annotated:
Services.AddInjector<TReportService>(
procedure(const Instance: TReportService; const Provider: IServiceProvider)
begin
Instance.Storage := Provider.GetRequiredService<IStorage>;
end);It is a policy of the type — it reaches descendants too — and it runs after the constructor and after the attribute injection, on every instance the container builds. It uses no RTTI, so it is the only route immune to a trimmed property RTTI.
Factory
Helper to build TServiceFactory from a class or instance:
Services.Add(TypeInfo(IMyService), Singleton, Factory.From(TypeInfo(IMyService), TMyImpl));TDuckDecorator<T>
Base class for wrapping a service with pre/post-call logic without inheritance. Override BeforeCall / AfterCall.
ILazy<T> y Lazy
The contract a consumer declares to defer one of its dependencies. The container supplies it; the service is registered as any other, and Value builds it on the first use.
constructor TReportService.Create(const AArchive: ILazy<IArchive>);Lazy.From<T> is the manual form, for what does not go through a constructor: it takes a factory and returns T itself.
Result := Lazy.From<IHeavy>(Provider,
function(P: IServiceProvider): IHeavy
begin
Result := THeavy.Create;
end);TActivatorUtilities
Building an object the container does not have registered, with its constructor arguments resolved:
uses Daf.DependencyInjection.ActivatorUtilities;
var Builder := TActivatorUtilities.CreateInstance<TReportBuilder>(Provider);
try
Builder.Run;
finally
Builder.Free; // not a registered service: the caller owns it
end;Among the public constructors whose arguments the provider can satisfy, the one with the most parameters is used, and two that tie make it fail naming the class and both. A mark makes one the only candidate:
[ActivatorUtilitiesConstructor]
constructor Create(const Storage: IStorage);⚠️ If the arguments of the marked constructor cannot all be resolved, CreateInstance fails naming the parameter, and does not fall back to another constructor. The container ignores the mark when it builds a registered class.
InjectInto(Provider, Instance) fills the [DafInject] properties of an instance the container did not build — a form the platform creates, for one. It takes no ownership: closing the container neither destroys that instance nor clears its properties.
Settings a service asks for by constructor
A service declares in its constructor the settings it needs; the container builds them, owns them and frees them. Nobody writes a Free:
uses Daf.Options;
AppOptions.Configure<TCacheSettings>(Services,
procedure(S: TCacheSettings)
begin
S.Host := 'localhost';
S.MaxItems := 500;
end);
Services.AddSingleton<ICache, TCache>;constructor TCache.Create(const Options: IOptions<TCacheSettings>);
begin
inherited Create;
FHost := Options.Value.Host;
FMaxItems := Options.Value.MaxItems;
end;AppOptions.Configure fills the settings with code — no file, no environment variable, nothing to read. Values computed at startup, or written by a test, come in through the same door as values read from a file, and the service that receives them cannot tell where they came from. Filling them from a configuration section is ConfigOptions.Bind, in Configuration.
The value is built on first use rather than when it is registered, and built once, also when several threads read it for the first time at once: settings the run never gets to use read nothing and validate nothing.
Configurations accumulate. Each AppOptions.Configure and each ConfigOptions.Bind of a settings type is one more IConfigureOptions<T>: all of them run, in registration order, on the same object, and each sees what the earlier ones left. A configuration registered directly as IConfigureOptions<T> runs in its turn.
The value has an owner, and this is where it bites
Value hands back a raw object, and its owner is the option. Delphi has no garbage collector, so a consumer that keeps the object and lets go of the option is left with a dangling pointer — a hazard that does not exist where this pattern is ported from.
Two correct ways to consume it, and no third:
// hold the option in a field, and read Value whenever it is needed
FOptions := Options;
// or copy what you need in the constructor, which is what most services want
FHost := Options.Value.Host;Validation runs when the value is built, and names the setting
AppOptions.Configure<TCacheSettings>(Services,
procedure(S: TCacheSettings)
begin
S.MaxItems := Cfg.MaxItems;
end,
function(const S: TCacheSettings; out Error: string): Boolean
begin
Result := S.MaxItems > 0;
if not Result then
Error := Format('MaxItems must be greater than zero, and it is %d', [S.MaxItems]);
end);It runs once, when the value is built and after every configuration, and a failure raises EOptionsValidationError naming the setting. Every validation of the type runs — those passed to Configure or Bind, and each IValidateOptions<T> registered directly — and one error gathers the reasons of all of them, joined by '; '. A validator answers with a TValidateOptionsResult: Success, Skip, which does not count as a failure, or Fail with one reason or several. Its Name is always the empty string: settings have no names.
type
TCacheRules = class(TInterfacedObject, IValidateOptions<TCacheSettings>)
public
function Validate(const Name: string; const Options: TCacheSettings): TValidateOptionsResult;
end;
Services.AddSingleton<IValidateOptions<TCacheSettings>>(
function(Provider: IServiceProvider): IValidateOptions<TCacheSettings>
begin
Result := TCacheRules.Create;
end);Asking for settings nobody registered fails naming the type. This diverges from the framework being ported: in .NET a single open registration serves every settings type at once, so asking for one nobody configured hands back a freshly built object with everything at zero and says nothing.
IOptions<TSection> is not derived from a parent settings class that has a property of that type. .NET does not do it either. The section is registered against its own path, and if both are registered, the parent’s nested object and the section’s are two distinct instances.
Settings with object members free what is theirs
A settings class with members of object type frees them itself. Options frees the root object and nothing else. See Configuration’s guide, where the binder builds those members.
Registering a generic contract once
A generic contract can be registered with its type arguments left open, and that one registration serves every closure of it:
Services.AddSingleton<IRepo<_>, TRepo<_>>;
// nothing else is registered, and both of these resolve
var Users := Provider.GetRequiredService<IRepo<TUser>>;
var Orders := Provider.GetRequiredService<IRepo<TOrder>>;_ is the placeholder declared in Daf.Rtti. IRepo<_> is an ordinary closed instantiation with a type of its own, so registering it needs nothing new — what the container learns is to match when it resolves. Three rules follow:
- the lifetime counts per closed type: an open singleton is one instance per closure, not one in total
- a registration written for a concrete closure always wins over the open one
- name and arity identify the contract, so one of the same name and a different arity does not match
_ClosedArgs, or how the implementation learns its closure
Delphi closes generics at compile time. Inside an implementation registered open, T is the placeholder: TypeInfo(T) and T.Create are of no use there — the placeholder’s constructor says so out loud instead of quietly building one. Everything the implementation needs about the real type arrives through a field of a fixed name that the container fills before the constructor runs, because that is where a service decides most of what it is:
type
TRepoArgs = record
T1: TEntityClass; // the bound, expressed by the field's type
end;
TRepo<T: class> = class(TInterfacedObject, IRepo<T>)
private
_ClosedArgs: TRepoArgs;
public
constructor Create;
end;
constructor TRepo<T>.Create;
begin
inherited Create;
FTable := _ClosedArgs.T1.ClassName; // TUser, TOrder, ...
end;Declaring it is optional. An implementation that needs nothing from the closure — a null implementation, a container that only holds — does not declare the field and pays nothing for it. Not declaring it is exactly the same as declaring one position per type argument and typing every one of them TClass: nothing to fill, and no bound to check. Note what that does not say — the argument does not lose its real class, the closure still names it. What is missing is the restriction.
Seven things to know about that field, and every one of them is a way to get it wrong:
- It is read, and never assigned. The
_prefix means “somebody else operates this”, as in_AddRef— here with the arrow the other way round: you declare it, the container writes it. Reading it is free and is what it is for. - Position binds; the name only documents. Delphi emits nothing about the names of a generic’s parameters, so the container binds the record’s fields to the type arguments in declaration order. A record with its fields the other way round compiles and does the opposite.
- The field’s type is the bound.
class of TEntitysays “only entities”, and a closure that does not satisfy it is refused naming the argument and the bound it fails to meet. TClassmeans no bound — and also no dispatch.SomeTClass.CreatecallsTObject.Createand never the class’s own constructor, not even a virtual one, becauseTClassisclass of TObjectandTObject.Createis not virtual. It hands back an object of the right class with its fields at zero, which reads like success. Type each field as narrowly as you can; where there really is no bound, build withTActivator.CreateInstance.- The record goes outside the class. Inside it,
T1is already the generic parameter:E2004 Identifier redeclared. - Its visibility is yours to choose, and
privateis the one to choose. The container writes it and the class itself reads it; nobody outside has anything to do with it, so putting it on the surface of the type widens it for nothing. The default RTTI settings bound methods and properties to public and published — for fields they include all four visibilities — so the container finds it in the most restrictive one there is. - Mind where the field sits in the class. In Delphi a
class varblock stays open until the next visibility specifier, so a field written after one is a class var too — invisible to RTTI, and shared by every closure. This is Delphi, not DAF: the container sees no field at all, writes nothing, and_ClosedArgsreadsnilin the constructor. Put a visibility specifier between the two.
What this is for, and what it is not
It serves forwarders and containers that are agnostic about the type. ILogger<T> is the canonical case: it holds the real logger and its only relationship with the argument is using its name as the category. It does not serve anything that needs the type as a type beyond what _ClosedArgs can hand over.
The test takes a finger. Cover every T in the body of the implementation: if the code still stands, it is registrable with its arguments left open. If something needed T to be T — a T.Create, a TypeInfo(T), a variable of type T holding a real value — it is not.
And three limits:
- All the arguments or none.
IFoo<TKnown, _>, fixing some and leaving others open, is not supported. - The argument has to travel as a reference. A contract where
Tappears in a position whose representation depends onT— returned by value, a record, an integer — cannot be served this way. The container compares the two contracts method by method and refuses, naming the method. It refuses whatever the implementation declares,_ClosedArgsor not — what makes a match safe is a property of the two contracts. To know in advance, run the finger test on the contract rather than on the implementation’s body: read the signatures, and ifTappears anywhere other than as an object, an interface or a class reference, the contract is not registrable with its arguments left open. - Matching is by name and arity, not by identity. Two generic families with the same simple name and the same arity, declared in different units, match. What sits underneath is not nothing — that same method-by-method check refuses the match, naming the first method that does not fit — but it is a net, not a check of identity.
A hand-written factory can be told what it is being asked for
The class route above fills _ClosedArgs before the constructor runs, so an implementation registered open knows its closure. A hand-written factory that takes only the provider does not: one factory serves every closure and returns the same object to all of them – an ILogger<TInvoices> writing under another closure’s category, with nothing to complain about.
A second factory shape receives the type being resolved as well as the provider:
Services.AddSingleton<IRepo<_>>(
function(P: IServiceProvider; RequestedType: PTypeInfo): IRepo<_>
begin
Result := TRepo.Create(RequestedType); // the closed IRepo<TUser>, IRepo<TOrder>, ...
end);The overload is chosen by how many parameters the lambda declares – a one-parameter factory is still a one-parameter factory, and nothing else changes. AddScoped, AddTransient and the three TryAdd* have the same overload.
It is the type being asked for, not the closure. For an open registration it is the closed type resolved (IRepo<TUser>); for an ordinary registration it is the service type itself (IRepo). The datum means the same in both – “this is what is being resolved” – so a single factory registered for several contracts can also tell them apart.
The two forms are separate types, not one with an optional parameter. A procedural type demands an identical parameter list, so a lambda whose arity does not match the type it is assigned to fails to compile with E2010 Incompatible types.
A factory that does not ask is not a special case. The short form is what almost every registration uses, and most have nothing to ask. One that takes only the provider registers and resolves as usual; registered open, it serves the same object to every closure.
Dependency
Daf.Commons only: Daf.Rtti for the _ placeholder and the RTTI helpers, Daf.Activator for the TActivator.CreateInstance named above, and Daf.Enumerable for the IInterfaceList<T> that GetServices hands back.
The implementation is in DependencyInjection.