# DependencyInjection
🌍 Idioma: English | Español
Contenedor IoC/DI para Delphi con una API fluida inspirada en Microsoft.Extensions.DependencyInjection de .NET. Registra servicios una vez, resuélvelos en cualquier parte — con tiempos de vida Singleton, Scoped y Transient.
¿Por qué usarlo?
- 🔌 API familiar — Los mismos métodos
AddSingleton/AddTransient/AddScopedque en .NET - 🎯 Genérico y type-safe — Verificación en tiempo de compilación mediante genéricos, sin tokens de cadena
- ⚡ Tres modos de vida — Singleton, Scoped (por scope) y Transient (nueva instancia siempre)
- 🏭 Registro flexible — Por clase, por lambda de factoría o por instancia existente
- 💤 Resolución diferida —
ILazy<T>en el constructor del consumidor rompe un ciclo entre dos servicios; el registro no se toca yValuepregunta al contenedor en cada uso - 🦆 Duck Typing —
TDuckDecorator<T>envuelve cualquier objeto que implemente la misma forma de interfaz - 📦 Sin dependencias externas —
DependencyInjection.AbstractionsyCommons, las dos de DAF
Inicio rápido
uses
Daf.Extensions.DependencyInjection,
Daf.DependencyInjection;
var
Services: IServiceCollection;
Provider: IServiceProvider;
begin
Services := TServiceCollection.Create;
// Registrar
Services.AddSingleton<IMyService, TMyService>;
// Construir el contenedor (congela la colección)
Provider := Services.BuildServiceProvider;
// Resolver
var Svc := Provider.GetRequiredService<IMyService>;
Svc.DoWork;
// Siempre hacer Shutdown al finalizar
Provider.ShutDown;
end;Sobrecargas de registro
// Por clase de implementación
Services.AddSingleton<IMyService, TMyServiceImpl>;
// Por lambda de factoría (control total)
Services.AddTransient<IMyService>(
function(P: IServiceProvider): IMyService
begin
Result := TMyServiceImpl.Create(P.GetRequiredService<IConfig>);
end);
// Por instancia existente (solo Singleton)
Services.AddSingleton<IMyService>(MyExistingInstance);Servicios con clave
Dos implementaciones del mismo contrato, distinguidas por una clave. Los mismos nombres que 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>('nadie'); // nilAddKeyedSingleton, AddKeyedScoped y AddKeyedTransient vienen en las mismas formas que sus Add* —por clase, por factoría y por instancia— y cada uno tiene su TryAddKeyed*.
La clave enumerada es la forma recomendada: renombrar un miembro llega a todos sus usos, y un typo es un error de compilación. Una clave de cadena como 'bolb' compila.
Una clave puede ser una cadena, un entero, un booleano, un valor de un enumerado, una metaclase o un PTypeInfo. Dos claves son la misma cuando coinciden su tipo y su valor, así que dos enumerados de tipos distintos que compartan ordinal son claves distintas, y la cadena '42' no es el entero 42. Cualquier otra cosa —un registro, un objeto— se rechaza donde entra, nombrando el tipo: el contenedor no puede definir una igualdad que esos tipos no tienen.
⚠️ Un carácter no es una clave. Una clave de un carácter es una clave de cadena: usa una cadena.
Los dos espacios están aislados
Un registro hecho con clave no se alcanza sin ella, y al revés. Un contrato con clave es un contrato distinto:
GetServices<T>no incluye los registros con clave, yCanResolvesin clave responde solo por los que no la tienen.ReplaceyRemoveAllalcanzan únicamente los registros sin clave.RemoveAllKeyedtoma la clave.TryAdd*pregunta por el par, no por el tipo: una clave no bloquea a otra, y ninguna bloquea al registro sin clave.Decorateenvuelve los registros sin clave y deja intactos los que la tienen. Decorar un tipo que solo tiene registros con clave falla diciendo que no hay nada registrado para él.
Pedir una clave desde un constructor o una propiedad
La misma marca sirve a las dos posiciones, y la clave viaja dentro de ella:
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] admite una cadena, un entero, un booleano, una metaclase o un TypeInfo(...). Para una clave enumerada, la aplicación declara su propia marca en una línea:
type
StorageInjectAttribute = class(DafInjectAttribute<TStorageKind>);El contenedor la reconoce por descendencia, así que nunca necesita conocer ese enumerado.
⚠️ Esa marca va en una unit propia, separada de las clases que la usan. Declarada en la misma unit, leer el atributo aborta con un access violation.
⚠️ Y el genérico no se escribe nunca dentro de un [ ]. [DafInject<TStorageKind>(...)] compila, con un W1074 Unknown custom attribute que casi nadie lee, y en ejecución la propiedad no tiene ningún atributo: la inyección no ocurre y nada lo dice.
Para una clave enumerada suelta, sin declarar marca, queda el escape [DafInject(TypeInfo(TStorageKind), Ord(FileSystem))]. El ordinal se comprueba contra el rango del tipo, y uno que no le pertenece falla nombrando la propiedad.
Un registro para cualquier clave
Un registro bajo ServiceKey.Any atiende toda clave de su tipo de servicio que no tenga registro propio, y su factoría recibe cuál se pidió:
Services.AddKeyedSingleton<IStorage>(ServiceKey.Any,
function(P: IServiceProvider; const Key: TValue): IStorage
begin
Result := TStorage.Create(Key.ToString);
end);Un registro concreto le gana —al comodín se le pregunta solo al fallar la búsqueda directa— y cada clave que atiende recibe su propia instancia. El centinela es un valor de un tipo declarado dentro del módulo de DI, y no se puede construir desde fuera.
⚠️ Un registro con clave no puede dejar sus argumentos genéricos sin fijar. Los dos emparejamientos no se combinan, y el registro se rechaza en el acto.
Inyección por propiedad
El constructor es lo normal, y esto no lo sustituye. Dos casos piden la inyección por propiedad:
- Dos servicios que se necesitan mutuamente. Un ciclo por constructor es inconstruible: ninguno de los dos puede correr antes de que el otro termine. Por propiedad se resuelve.
- Una instancia que el contenedor no construye. Un formulario lo construye la plataforma, así que no hay constructor que el contenedor pueda elegir.
Cada propiedad se declara a sí misma, y una clase participa por tener alguna: no hay nada más que declarar:
type
TReportService = class(TInterfacedObject, IReportService)
private
FStorage: IStorage;
public
[DafInject]
property Storage: IStorage read FStorage write FStorage;
end;La propiedad ha de ser de tipo interfaz y escribible; da igual si la respalda un campo o un método, y da igual que ese campo sea strict private. Una propiedad marcada que no se puede rellenar aborta nombrando la propiedad, su tipo y la clase.
El constructor no las ve. La inyección ocurre cuando el constructor ya ha terminado, así que una propiedad marcada vale nil dentro de él. Una dependencia que haga falta durante la construcción va por el constructor; las dos vías conviven en la misma clase.
Solo se cablea lo que el contenedor construye a partir de una clase. Lo que devuelve una factory, y una instancia registrada como tal, son de quien los construyó: el contenedor no les rellena ninguna propiedad, no les ejecuta ningún cableado por código y no les vacía las propiedades al cerrar. Su dueño los cablea a petición con InjectInto. Cuando se decora un servicio registrado por clase, el original llega al decorador ya cableado.
Cablear una instancia que el contenedor no construyó
TActivatorUtilities.InjectInto(Provider, Self); // en el constructor del formularioLas mismas marcas, las mismas exigencias y los mismos errores que una resolución. El contenedor no toma posesión de lo que cablea: no lo cuenta, cerrarlo no lo destruye, y los servicios que sostenga viven mientras él los sostenga.
Cablear por código, sin marcar nada
Services.AddInjector<TReportService>(
procedure(const Instance: TReportService; const Provider: IServiceProvider)
begin
Instance.Storage := Provider.GetRequiredService<IStorage>;
end);Para lo que no se puede anotar: una clase de otra librería, una unit con la RTTI de propiedades recortada —esta vía no usa RTTI en absoluto, así que es la única inmune— y una clave de servicio incómoda de escribir dentro de un atributo. Es política del tipo, así que alcanza también a sus descendientes, y varios cableados corren en el orden en que se registraron. Corre después del constructor y después de la inyección por atributo, y solo sobre lo que el contenedor construye.
⚠️ Una RTTI de propiedades recortada apaga esto, y nada lo dice. {$RTTI EXPLICIT PROPERTIES([])} deja las propiedades de una unit fuera de la RTTI, y entonces una marcada no existe para el barrido: la clase parece cualquier otra que no marca nada, así que se resuelve con su dependencia en nil y el fallo aparece en el primer uso. El contenedor no puede detectarlo. La directiva la escribe alguien a mano en la cabecera de esa misma unit, y la salida es el cableado por código, que no usa RTTI.
Scopes
var Scope := Provider.CreateScope;
try
var Svc := Scope.ServiceProvider.GetRequiredService<IScopedService>;
// Las instancias Scoped son únicas dentro de este scope
finally
Scope := nil; // dispara Shutdown + limpieza gestionada
end;Hilos
Una vez construido el proveedor, resolver desde varios hilos a la vez es seguro. Cada registro se construye una sola vez y en un solo hilo, y todos los hilos que lo pidan reciben esa misma instancia — así que un constructor no tiene por qué ser seguro entre hilos.
Lo que esa garantía cubre es el contenedor, no lo que sale de él: un servicio resuelto es tan seguro entre hilos como lo haya hecho quien lo escribió.
Componer es de un solo hilo. Se registra en un hilo, se construye el proveedor, y desde entonces la colección solo se lee — registrar después de construir falla diciendo que está congelada. No hay garantía frente a componer desde dos hilos.
Un ámbito es una frontera de vida, no un hilo. Un ámbito se puede usar desde cualquier hilo, y desde varios a la vez, lo haya creado quien sea. Un ámbito por unidad de trabajo sigue siendo la recomendación, pero el motivo son los servicios y no el contenedor: un servicio con estado mutable no se vuelve concurrente por vivir en un ámbito. Y un ámbito por hilo no aísla los singletons — el ámbito de singletons es uno para todos, así que dos hilos con ámbitos propios lo siguen compartiendo.
Cerrar es seguro frente a resoluciones en curso. Un ShutDown concurrente con una resolución termina de una de dos formas: la resolución se completa y entrega su servicio, o falla con EContainerShutDownError. Si ya había construido su instancia cuando perdió la carrera, esa instancia se destruye en vez de entregarse.
⚠️ Un constructor que resuelve en OTRO hilo y espera el resultado se interbloquea. La construcción de un registro está serializada, así que el hilo que espera tiene tomado lo que el otro necesita. Ningún contenedor previene esto; pide la dependencia por el constructor.
Patrón AppModule
Organiza los registros por módulo:
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;Se aplican por nombre, en el orden escrito — AddModule<T> instancia el módulo y llama a su AddServices en el acto, así que nadie llama a AddServices a mano:
Services
.AddModule<TMyModule>
.AddModule<TOtherModule>;Consulta DependencyInjection.Abstractions para la interfaz IAppModule y todos los contratos clave.
Documentación
- 📖 Guía de uso — tiempos de vida, scopes, Lazy, DuckDecorator, integración con Hosting
- 📖 Composición del arranque — módulos y funciones
AddXxx,TryAdd/Replace/Decorate,IStartupTask, validación del grafo - 📁 ModuleComposition — tres módulos aplicados en orden, y el que solo puede entrar por la sobrecarga de instancia
Abstracciones
DependencyInjection.Abstractions
🌍 Idioma: English | Español
Contratos principales (interfaces y tipos) del sistema de Inyección de Dependencias de DAF. Este módulo no contiene implementación — define la forma de la que dependen los consumidores y que DependencyInjection implementa.
Qué hay en este módulo
| Unidad | Contenido |
|---|---|
Daf.Extensions.DependencyInjection |
IServiceCollection, IServiceProvider, IServiceScope, IAppModule, tiempos de vida, Factory, TDuckDecorator<T>, ILazy<T> y Lazy |
Daf.Extensions.Options |
IOptions<T>, IConfigureOptions<T>, IValidateOptions<T>, TValidateOptionsResult, TOptionsValidator<T>, EOptionsValidationError — el contrato del patrón de opciones |
Daf.Options |
TOptions<T>, TConfigureOptions<T>, TValidateOptions<T> y AppOptions — ajustes registrados con código |
Daf.AppModule |
Alias de compatibilidad de IAppModule: el contrato se mudó junto a AddModule<T>, que es método del record IServiceCollection y no puede depender de esta unit. Un uses Daf.AppModule que ya existiera sigue valiendo |
Daf.DependencyInjection.ActivatorUtilities |
TActivatorUtilities: construir un objeto con sus argumentos de constructor resueltos, e InjectInto para cablear una instancia que el contenedor no construyó |
Daf.DependencyInjection.OpenGenerics |
Emparejamiento, comprobaciones y activación de los registros que dejan sus argumentos de tipo sin fijar |
Tipos clave
IServiceCollection
Constructor fluido para registrar servicios antes de construir el contenedor.
Services
.AddSingleton<ICache, TMemoryCache>
.AddTransient<IRepo, TRepoImpl>
.AddScoped<IUoW, TDbUoW>;
var Provider := Services.BuildServiceProvider;IServiceProvider
El contenedor resuelto. Úsalo para obtener instancias de servicios.
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
Frontera de tiempo de vida. Los servicios Scoped se comparten dentro de un scope y se liberan cuando este termina.
var Scope := Provider.CreateScope;
var Svc := Scope.ServiceProvider.GetRequiredService<IScopedService>;
Scope := nil; // libera las instancias ScopedIAppModule
Convención para agrupar registros de servicios:
type
IAppModule = interface(IInvokable)
procedure AddServices(const ServiceCollection: IServiceCollection);
end;ServiceKey y TServiceSlot
ServiceKey es todo lo que el contenedor sabe de una clave de servicio: si un TValue puede serlo, cómo se comparan dos, cómo se nombra una en un mensaje, y cuál es el comodín.
ServiceKey.IsValid(Key); // una cadena, un entero, un booleano, un enumerado, una metaclase
// o un PTypeInfo -- y la vacía, que significa "sin clave"
ServiceKey.Check(Key); // aborta nombrando el tipo cuando no puede serlo
ServiceKey.Text(Key); // para un mensaje, y para nada más
ServiceKey.Any; // la clave comodínDos claves son la misma cuando coinciden su tipo y su valor, así que dos enumerados de tipos distintos que compartan ordinal son claves distintas. El rechazo ocurre donde la clave entra —al registrar y al resolver— y nunca al comparar dos claves.
TServiceSlot es el par (tipo de servicio, clave) por el que indexa la colección. Una clave vacía es el espacio sin clave, no un tercer estado.
TServiceKeyedFactory<T>
TServiceKeyedFactory<ServiceType: IInterface> =
reference to function(Provider: IServiceProvider; const Key: TValue): ServiceType;Una factoría enterada de qué clave se le está pidiendo. Es lo que hace útil un registro comodín: lo único que puede hacer una factoría que atiende a todas las claves es decidir en función de cuál le toca.
DafInject
La marca de la inyección por propiedad: va en cada propiedad que el contenedor rellena, y una clase participa por tenerla. No hay nada que declarar en la clase.
TReportService = class(TInterfacedObject, IReportService)
private
FStorage: IStorage;
public
[DafInject]
property Storage: IStorage read FStorage write FStorage;
end;La propiedad ha de ser de tipo interfaz y escribible. Una propiedad marcada que no se puede rellenar levanta EPropertyInjectionError nombrando la propiedad, su tipo y la clase.
Una marca y no dos. Bajo una RTTI recortada la inyección está apagada y nada lo dice.
La clave viaja dentro de esta marca, y la misma marca sirve a un parámetro de constructor:
constructor Create([DafInject('audit')] const Audit: IAuditLog);[DafInject] admite una cadena, un entero, un booleano, una metaclase, un TypeInfo(...), o un TypeInfo(...) más un ordinal. No puede admitir un TValue: el argumento de un atributo tiene que ser una constante, y un TValue ahí da E2026 hasta con un literal de cadena — el TValue se construye dentro del constructor, al que llama la RTTI al leer el atributo.
Para una clave enumerada, la aplicación declara su propia marca sobre la base genérica, en una línea:
type
StorageInjectAttribute = class(DafInjectAttribute<TStorageKind>);El barrido la reconoce por descendencia (Attr is DafInjectAttribute), así que el framework nunca necesita conocer ese enumerado. ⚠️ Esa marca va en una unit propia, separada de las clases que la usan: declarada en la misma unit, leer el atributo aborta con un access violation. ⚠️ Y el genérico no se escribe nunca dentro de un [ ] — [DafInject<TStorageKind>(...)] compila con un W1074 y en ejecución la propiedad no tiene ningún atributo.
TServiceInjector<T> y AddInjector<T>
El cableado por código, para lo que no se puede anotar:
Services.AddInjector<TReportService>(
procedure(const Instance: TReportService; const Provider: IServiceProvider)
begin
Instance.Storage := Provider.GetRequiredService<IStorage>;
end);Es política del tipo —alcanza también a sus descendientes— y corre después del constructor y después de la inyección por atributo, sobre toda instancia que el contenedor construya. No usa RTTI, así que es la única vía inmune a una RTTI de propiedades recortada.
Factory
Helper para construir un TServiceFactory desde una clase o instancia:
Services.Add(TypeInfo(IMyService), Singleton, Factory.From(TypeInfo(IMyService), TMyImpl));TDuckDecorator<T>
Clase base para envolver un servicio con lógica pre/post-llamada sin herencia. Sobreescribe BeforeCall / AfterCall.
ILazy<T> y Lazy
El contrato que un consumidor declara para diferir una de sus dependencias. Lo entrega el contenedor; el servicio se registra como cualquier otro, y Value lo construye en el primer uso.
constructor TReportService.Create(const AArchive: ILazy<IArchive>);Lazy.From<T> es la forma manual, para lo que no pasa por un constructor: recibe una factoría y devuelve la propia T.
Result := Lazy.From<IHeavy>(Provider,
function(P: IServiceProvider): IHeavy
begin
Result := THeavy.Create;
end);TActivatorUtilities
Construir un objeto que el contenedor no tiene registrado, con sus argumentos de constructor resueltos:
uses Daf.DependencyInjection.ActivatorUtilities;
var Builder := TActivatorUtilities.CreateInstance<TReportBuilder>(Provider);
try
Builder.Run;
finally
Builder.Free; // no es un servicio registrado: el dueño es quien llama
end;De los constructores públicos cuyos argumentos el proveedor puede satisfacer se usa el de más parámetros, y dos que empatan lo hacen fallar nombrando la clase y los dos. Una marca hace de uno el único candidato:
[ActivatorUtilitiesConstructor]
constructor Create(const Storage: IStorage);⚠️ Si los argumentos del constructor marcado no se pueden resolver todos, CreateInstance falla nombrando el parámetro, y no vuelve a otro constructor. El contenedor ignora la marca cuando construye una clase registrada.
InjectInto(Provider, Instance) rellena las propiedades [DafInject] de una instancia que el contenedor no construyó —un formulario que crea la plataforma, por ejemplo—. No toma posesión: cerrar el contenedor ni destruye esa instancia ni le vacía las propiedades.
Ajustes que un servicio pide por constructor
Un servicio declara en su constructor los ajustes que necesita; el contenedor los construye, es su dueño y los libera. Nadie escribe un 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 rellena los ajustes con código — sin fichero, sin variable de entorno, sin nada que leer. Unos valores calculados en el arranque, o escritos por una prueba, entran por la misma puerta que los que vienen de un fichero, y el servicio que los recibe no distingue de dónde salen. Llenarlos desde una sección de configuración es ConfigOptions.Bind, en Configuration.
El valor se construye en el primer uso, no al registrarse, y una sola vez, también cuando varios hilos lo leen por primera vez a la vez: unos ajustes que la ejecución no llega a usar no leen ni validan nada.
Las configuraciones se acumulan. Cada AppOptions.Configure y cada ConfigOptions.Bind de un tipo de ajustes es un IConfigureOptions<T> más: corren todas, en orden de registro, sobre el mismo objeto, y cada una ve lo que dejaron las anteriores. Una configuración registrada directamente como IConfigureOptions<T> corre en su turno.
El valor tiene dueño, y aquí es donde muerde
Value devuelve un objeto crudo, y su dueño es la opción. Delphi no tiene recolector, así que quien se guarde el objeto y suelte la opción se queda con un puntero colgando — un riesgo que no existe allí de donde se porta esto.
Dos formas correctas de consumirlo, y no hay una tercera:
// sostener la opción en un campo, y leer Value cuando haga falta
FOptions := Options;
// o copiar lo que se necesite en el constructor, que es lo que quiere casi todo servicio
FHost := Options.Value.Host;La validación corre al construir el valor, y nombra el ajuste
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);Corre una vez, al construirse el valor y después de todas las configuraciones, y un fallo levanta EOptionsValidationError nombrando el ajuste. Corren todas las validaciones del tipo —las que se pasan a Configure o a Bind, y cada IValidateOptions<T> registrado directamente—, y un solo error reúne los motivos de todas, unidos por '; '. Un validador responde con un TValidateOptionsResult: Success, Skip, que no cuenta como fallo, o Fail con uno o varios motivos. Su Name es siempre la cadena vacía: los ajustes no tienen nombre.
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);Lo que nadie registró no se inventa
Pedir unos ajustes que nadie registró falla nombrando el tipo. Es una divergencia con el original que se porta: en .NET un único registro sin fijar sirve a todos los tipos de ajustes a la vez, así que pedir unos que nadie configuró devuelve un objeto recién construido con todo a cero, sin decir nada.
IOptions<TSeccion> no se deriva de una clase de ajustes padre que tenga una propiedad de ese tipo. .NET tampoco lo hace. La sección se registra contra su propia ruta, y si se registran las dos, el objeto anidado del padre y el de la sección son dos instancias distintas.
Unos ajustes con miembros objeto liberan lo suyo
Una clase de ajustes con miembros de tipo objeto los libera ella. La opción libera el objeto raíz y nada más. Ver la guía de Configuration, que es donde el enlazador construye esos miembros.
Registrar un contrato genérico una sola vez
Un contrato genérico se puede registrar sin fijar sus argumentos de tipo, y ese único registro sirve a todos sus cierres:
Services.AddSingleton<IRepo<_>, TRepo<_>>;
// no hay nada más registrado, y estos dos resuelven
var Users := Provider.GetRequiredService<IRepo<TUser>>;
var Orders := Provider.GetRequiredService<IRepo<TOrder>>;_ es el marcador que declara Daf.Rtti. IRepo<_> es una instanciación cerrada corriente con su propio tipo, así que registrarla no necesita nada nuevo — lo que el contenedor aprende es a emparejar al resolver. De ahí salen tres reglas:
- el ciclo de vida cuenta por tipo cerrado: un singleton sin fijar es una instancia por cierre, no una en total
- un registro escrito para un cierre concreto gana siempre al registro sin fijar
- el nombre y la aridad identifican el contrato, así que uno del mismo nombre y otra aridad no empareja
_ClosedArgs, o cómo se entera la implementación de cuál es su cierre
Delphi cierra los genéricos al compilar. Dentro de una implementación registrada sin fijar, T es el marcador: TypeInfo(T) y T.Create no sirven de nada ahí — el constructor del marcador lo dice en voz alta en vez de construir uno en silencio. Todo lo que la implementación necesite del tipo de verdad le llega por un campo de nombre fijo que el contenedor rellena antes de que corra el constructor, que es donde un servicio decide casi todo lo que es:
type
TRepoArgs = record
T1: TEntityClass; // la cota, expresada por el tipo del campo
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;Declararlo es opcional. Una implementación que no necesita nada del cierre —una implementación nula, un contenedor que solo guarda— no declara el campo y no paga nada por ello. No declararlo es exactamente igual que declararlo con una posición por argumento de tipo y todas de tipo TClass: nada que rellenar, y ninguna cota que comprobar. Y fíjate en lo que eso no dice — el argumento no pierde su clase real, el cierre la sigue nombrando. Lo que falta es la restricción.
Siete cosas que saber de ese campo, y cada una es una forma de equivocarse:
- Se lee, y no se asigna nunca. El prefijo
_significa «esto lo opera otro», como en_AddRef— aquí con la flecha al revés: lo declaras tú, lo escribe el contenedor. Leerlo es gratis y es para lo que está. - Liga la posición; el nombre solo documenta. Delphi no emite nada sobre los nombres de los parámetros de un genérico, así que el contenedor ata los campos del record a los argumentos de tipo por orden de declaración. Un record con los campos al revés compila y hace lo contrario.
- El tipo del campo es la cota.
class of TEntitydice «solo entidades», y un cierre que no la satisface se rechaza nombrando el argumento y la cota que incumple. TClasssignifica sin cota — y también sin despacho.UnTClass.Createllama aTObject.Createy nunca al constructor propio de la clase, ni siquiera declarándolovirtual, porqueTClassesclass of TObjectyTObject.Createno lo es. Devuelve un objeto de la clase correcta con los campos a cero, que se lee como un acierto. Tipa cada campo tan estrecho como puedas; donde de verdad no haya cota, construye conTActivator.CreateInstance.- El record va fuera de la clase. Dentro,
T1ya es el parámetro genérico:E2004 Identifier redeclared. - La visibilidad la eliges tú, y
privatees la que hay que elegir. Lo escribe el contenedor y lo lee la propia clase; nadie de fuera tiene nada que hacer con él, así que ponerlo en la superficie del tipo es ensancharla a cambio de nada. Las directivas RTTI por defecto acotan a public y published los métodos y las propiedades —para los campos incluyen las cuatro visibilidades—, así que el contenedor lo encuentra en la más restrictiva que hay. - Cuidado con dónde cae el campo dentro de la clase. En Delphi un bloque
class varsigue abierto hasta el siguiente especificador de visibilidad, así que un campo escrito detrás es también un class var — que la RTTI no ve y que comparten todos los cierres. Esto es Delphi, no DAF: el contenedor no ve campo ninguno, no escribe nada, y_ClosedArgssale anilen el constructor. Pon un especificador de visibilidad entre los dos.
Para qué sirve esto, y para qué no
Sirve para reenviadores y contenedores agnósticos del tipo. ILogger<T> es el caso canónico: sostiene el registrador de verdad y su única relación con el argumento es usar su nombre como categoría. No sirve para nada que necesite el tipo como tipo más allá de lo que _ClosedArgs puede darle.
La prueba se hace con un dedo. Tapa cada T del cuerpo de la implementación: si el código sigue en pie, es registrable sin fijar. Si algo necesitaba que T fuera T — un T.Create, un TypeInfo(T), una variable de tipo T con un valor de verdad dentro —, no lo es.
Y tres límites:
- Todos los argumentos o ninguno.
IFoo<TConocido, _>, fijando unos y dejando otros sin fijar, no está soportado. - El argumento tiene que viajar como referencia. Un contrato donde
Taparezca en una posición cuya representación dependa deT—devuelto por valor, un record, un entero— no se puede servir por esta vía. El contenedor compara los dos contratos método a método y lo rechaza, nombrando el método. Lo rechaza sea lo que sea lo que declare la implementación, con_ClosedArgso sin él — que un emparejamiento sea seguro es una propiedad de los dos contratos. Para saberlo de antemano, hazle la prueba del dedo al contrato en vez de al cuerpo de la implementación: lee las firmas, y siTaparece en algún sitio que no sea un objeto, una interfaz o una referencia de clase, el contrato no es registrable sin fijar. - El emparejamiento es por nombre y aridad, no por identidad. Dos familias genéricas del mismo nombre simple y la misma aridad, declaradas en units distintas, emparejan. Lo que queda debajo no es la nada —esa misma comparación método a método rechaza el emparejamiento nombrando el primer método que no encaja— pero es una red, no una comprobación de identidad.
Una factoría escrita a mano puede enterarse de qué le piden
La ruta por clase de arriba rellena _ClosedArgs antes de que corra el constructor, así que una implementación registrada sin fijar conoce su cierre. Una factoría escrita a mano que recibe solo el proveedor, no: una única factoría atiende todos los cierres y devuelve el mismo objeto a todos – un ILogger<TFacturas> escribiendo bajo la categoría de otro cierre, sin que nada se queje.
Un segundo tipo de factoría recibe, además del proveedor, el tipo que se está resolviendo:
Services.AddSingleton<IRepo<_>>(
function(P: IServiceProvider; RequestedType: PTypeInfo): IRepo<_>
begin
Result := TRepo.Create(RequestedType); // el cierre concreto IRepo<TUser>, IRepo<TOrder>, ...
end);La sobrecarga se elige por cuántos parámetros declara la lambda – una factoría de un parámetro sigue siendo de un parámetro, y no cambia nada más. AddScoped, AddTransient y los tres TryAdd* tienen la misma sobrecarga.
Es el tipo que se pide, no el cierre. Para un registro sin fijar es el tipo cerrado que se resuelve (IRepo<TUser>); para un registro corriente es el tipo del propio servicio (IRepo). El dato significa lo mismo en los dos – «esto es lo que se está resolviendo» – así que una única factoría registrada para varios contratos también puede distinguirlos.
Las dos formas son tipos distintos, no uno con un parámetro opcional. Un tipo procedural exige una lista de parámetros idéntica, así que una lambda cuya aridad no coincide con el tipo al que se asigna no compila – E2010 Incompatible types–.
Una factoría que no pregunta no es un caso especial. La forma corta es la que usa casi todo registro, y la mayoría no tienen nada que preguntar. Una que toma solo el proveedor se registra y resuelve como siempre; registrada sin fijar, sirve el mismo objeto a todos los cierres.
Dependencias
Solo Daf.Commons: Daf.Rtti por el marcador _ y los helpers de RTTI, Daf.Activator por el TActivator.CreateInstance que se nombra arriba, y Daf.Enumerable por el IInterfaceList<T> que devuelve GetServices.
La implementación está en DependencyInjection.