diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 610e1582..06e867ba 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -11,6 +11,13 @@ Este documento estabelece as regras de design e desenvolvimento do framework Hor * Para encerramentos coordenados e seguros (probes de saúde no Kubernetes/Load Balancers), use sempre `THorse.StopListenGraceful(TimeoutMS)`. * As propriedades `ActiveRequests` e `IsShuttingDown` estão expostas estaticamente no facade `THorse` e devem ser usadas em endpoints de `/health` ou monitoramento de observabilidade APM. +## 🟢 Gerenciamento e Injeção de Dependências (Request Scope) +* O Horse expõe a propriedade `Services` na classe de requisição `THorseRequest`, provendo um container IoC local e thread-safe para o escopo do request. +* Para serviços que devem ser destruídos automaticamente ao final da requisição (evitando vazamento de memória), registre-os usando `Req.Services.Add(TClass, Instance)`. +* Para inicialização sob demanda (Lazy Loading), use `Req.Services.AddFactory(TClass, FactoryMethod)`. A instância só será criada no momento da chamada de `Resolve`. +* Para obter um serviço previamente injetado, chame `Req.Services.Resolve(TClass)` e faça a coerção de tipo necessária. +* Nunca instancie dicionários de escopo ou de serviços paralelos dentro das closures de rotas; utilize sempre a infraestrutura nativa do `Services` para garantir o ciclo de vida e thread-safety coordenados pelo framework. + ## 🧪 Padrões de Testes e Concorrência * Ao escrever testes de integração que envolvam o encerramento do servidor ou simulação de tráfego, utilize sempre a biblioteca HTTP nativa do Delphi (`System.Net.HttpClient` e `System.Net.URLClient`) para garantir compatibilidade multiplataforma nativa no FPC (Lazarus/Linux) sem depender de pacotes externos. * Em testes de shutdown ou concorrência física, utilize o cabeçalho `Connection: close` na requisição do cliente HTTP para forçar a liberação imediata do socket no sistema operacional, evitando travamento de pools de conexão físicos. diff --git a/doc/dependency-injection.md b/doc/dependency-injection.md new file mode 100644 index 00000000..a95c30f6 --- /dev/null +++ b/doc/dependency-injection.md @@ -0,0 +1,160 @@ +# Contextual Dependency Injection (Request Scope) + +*Read this in [English](./dependency-injection.md) or [Português (BR)](./dependency-injection.pt-BR.md).* + +**Contextual Dependency Injection (Request Scope)** in Horse enables the deterministic lifecycle management of service instances and classes directly coupled to the active HTTP request lifecycle. + +By registering request-scoped services, developers ensure complete thread-safe state isolation between concurrent requests and benefit from the automatic disposal of instantiated resources at the end of the HTTP routing pipeline. This eliminates memory leaks and the need for manual `try/finally` blocks inside route closures. + +--- + +## 🗺️ Dependency Injection Lifecycle + +The lifecycle of the `Services` request property is described in the sequence diagram below: + +```mermaid +sequenceDiagram + autonumber + participant Client as HTTP Client + participant Horse as THorseRequest (Services) + participant Core as THorseRequestContext (Dictionary) + participant Disposer as Automatic Disposal + + Client->>Horse: HTTP Request starts + Note over Horse: Services (Lazy-initialized on first access) + + rect rgb(20, 20, 30) + Note over Horse: Services Registration + Horse->>Core: Req.Services.Add(TMyService, Instance) + Note right of Core: Registered as owned instance + end + + rect rgb(20, 30, 20) + Note over Horse: Services Resolution + Horse->>Core: Req.Services.Resolve(TMyService) + Core-->>Horse: Returns typed instance + end + + Client->>Horse: HTTP Routing pipeline finishes + Horse->>Disposer: THorseRequest.Clear or Destroy triggered + Disposer->>Core: Triggers FreeAndNil(FServices) + Note over Core: Destroys all owned instances (doOwnsValues) + Note over Core: Disposal Completed (Zero Memory Leaks!) +``` + +--- + +## 🛠️ Injection and Registration Modes + +The `Services` property offers two main ways to register dependencies, each with specific behaviors: + +### 1. Direct Instance Injection (`Add`) +Registers a previously created object instance in the context of the current request. By default, the context class takes ownership of the object and destroys it automatically at the end of the request. + +```delphi +Req.Services.Add(TMyService, TMyService.Create); +``` + +### 2. Lazy Injection via Factory (`AddFactory`) +Registers a factory delegate that defines how to create the service on-demand (*Lazy Loading*). The service is only physically instantiated at the exact moment it is resolved (when calling `Resolve`). Once instantiated, it is cached in the context of the current request and automatically destroyed when the request ends. + +```delphi +Req.Services.AddFactory(TMyService, + function: TObject + begin + Result := TMyService.Create; + end); +``` + +--- + +## 💻 Complete Practical Example + +```delphi +program ConsoleDependencyInjection; + +{$APPTYPE CONSOLE} + +uses + Horse, Horse.Commons, System.SysUtils; + +type + TMyService = class + private + FId: string; + public + constructor Create(const AId: string); + destructor Destroy; override; + function GetMessage: string; + end; + +{ TMyService } + +constructor TMyService.Create(const AId: string); +begin + inherited Create; + FId := AId; + Writeln(Format('[TMyService] Instantiated with ID: %s', [FId])); +end; + +destructor TMyService.Destroy; +begin + Writeln(Format('[TMyService] Destroyed with ID: %s (Automatically cleaned up)', [FId])); + inherited Destroy; +end; + +function TMyService.GetMessage: string; +begin + Result := 'Hello from a Contextual Service! ID: ' + FId; +end; + +begin + // Route 1: Using Direct Instance Injection + THorse.Get('/resolve', + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + LService := TMyService.Create('Direct'); + Req.Services.Add(TMyService, LService); + Next(); + end, + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + LService := TMyService(Req.Services.Resolve(TMyService)); + Res.Send(LService.GetMessage); + end); + + // Route 2: Using Lazy Factory (Lazy Loading) + THorse.Get('/lazy', + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + begin + Req.Services.AddFactory(TMyService, + function: TObject + begin + Result := TMyService.Create('Lazy'); + end); + Next(); + end, + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + // The factory will be executed and the service instantiated only on the line below! + LService := TMyService(Req.Services.Resolve(TMyService)); + Res.Send(LService.GetMessage); + end); + + THorse.Listen(9000); +end. +``` + +--- + +## 📈 Architectural Benefits + +1. **Deterministic and Automatic Lifecycle:** Ensures resources are safely disposed of at the end of the HTTP routing pipeline, eliminating memory leaks. +2. **Concurrent State Isolation:** Fully thread-safe, allowing each concurrent request to process its service instances in complete isolation, avoiding race conditions. +3. **Native Lazy Initialization:** Reduced RAM footprint and faster request processing overheads using `AddFactory`, instantiating services only when required by the executed route. diff --git a/doc/dependency-injection.pt-BR.md b/doc/dependency-injection.pt-BR.md new file mode 100644 index 00000000..fa038622 --- /dev/null +++ b/doc/dependency-injection.pt-BR.md @@ -0,0 +1,160 @@ +# Injeção de Dependência Contextual (Request Scope) + +*Read this in [English](./dependency-injection.md) or [Português (BR)](./dependency-injection.pt-BR.md).* + +O **Gerenciamento e Injeção de Dependência Contextual (Request Scope)** no Horse permite o gerenciamento determinístico do ciclo de vida de instâncias de serviços e classes acopladas diretamente ao ciclo de vida da requisição HTTP ativa. + +Ao registrar serviços em escopo de requisição, o desenvolvedor garante o isolamento completo de estado entre requisições concorrentes (thread-safe) e conta com o descarte automático dos recursos instanciados ao final do pipeline de roteamento HTTP, eliminando por completo vazamentos de memória (memory leaks) e a necessidade de blocos `try/finally` manuais nas closures de rotas. + +--- + +## 🗺️ Ciclo de Vida da Injeção de Dependências + +O ciclo de vida da propriedade `Services` na requisição segue a sequência descrita no diagrama abaixo: + +```mermaid +sequenceDiagram + autonumber + participant Cliente as Cliente HTTP + participant Horse as THorseRequest (Services) + participant Core as THorseRequestContext (Dicionário) + participant Destrutor as Destruição Automática + + Cliente->>Horse: Requisição HTTP iniciada + Note over Horse: Services (Lazy-initialized no primeiro acesso) + + rect rgb(20, 20, 30) + Note over Horse: Registro de Serviços + Horse->>Core: Req.Services.Add(TMyService, Instance) + Note right of Core: Registrado como instância pertencente + end + + rect rgb(20, 30, 20) + Note over Horse: Resolução de Serviços + Horse->>Core: Req.Services.Resolve(TMyService) + Core-->>Horse: Retorna instância tipada + end + + Cliente->>Horse: Pipeline de roteamento é finalizado + Horse->>Destrutor: THorseRequest.Clear ou Destroy é acionado + Destrutor->>Core: Dispara FreeAndNil(FServices) + Note over Core: Destrói todas as instâncias pertencentes (doOwnsValues) + Note over Core: Descarte Concluído (Zero Memory Leaks!) +``` + +--- + +## 🛠️ Modos de Injeção e Registro + +A propriedade `Services` fornece duas formas principais de registro de dependências com comportamentos específicos: + +### 1. Injeção de Instância Direta (`Add`) +Registra uma instância de objeto previamente criada no contexto da requisição corrente. Por padrão, a classe gerenciadora assume a propriedade (ownership) do objeto, descartando-o automaticamente ao final do request. + +```delphi +Req.Services.Add(TMyService, TMyService.Create); +``` + +### 2. Injeção Preguiçosa via Fábrica (`AddFactory`) +Registra um delegate de fábrica (factory method) que define como criar o serviço sob demanda (*Lazy Loading*). O serviço só é instanciado fisicamente no primeiro momento em que for resolvido (chamada de `Resolve`). Uma vez instanciado, ele é cacheado no contexto da requisição corrente e destruído automaticamente ao término da requisição. + +```delphi +Req.Services.AddFactory(TMyService, + function: TObject + begin + Result := TMyService.Create; + end); +``` + +--- + +## 💻 Exemplo Prático Completo + +```delphi +program ConsoleDependencyInjection; + +{$APPTYPE CONSOLE} + +uses + Horse, Horse.Commons, System.SysUtils; + +type + TMyService = class + private + FId: string; + public + constructor Create(const AId: string); + destructor Destroy; override; + function GetMessage: string; + end; + +{ TMyService } + +constructor TMyService.Create(const AId: string); +begin + inherited Create; + FId := AId; + Writeln(Format('[TMyService] Instanciado com ID: %s', [FId])); +end; + +destructor TMyService.Destroy; +begin + Writeln(Format('[TMyService] Destruído com ID: %s (Limpo de forma automática)', [FId])); + inherited Destroy; +end; + +function TMyService.GetMessage: string; +begin + Result := 'Olá de um Serviço Contextual! ID: ' + FId; +end; + +begin + // Rota 1: Usando Injeção de Instância Direta + THorse.Get('/resolve', + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + LService := TMyService.Create('Direto'); + Req.Services.Add(TMyService, LService); + Next(); + end, + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + LService := TMyService(Req.Services.Resolve(TMyService)); + Res.Send(LService.GetMessage); + end); + + // Rota 2: Usando Lazy Factory (Carregamento Preguiçoso) + THorse.Get('/lazy', + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + begin + Req.Services.AddFactory(TMyService, + function: TObject + begin + Result := TMyService.Create('Lazy'); + end); + Next(); + end, + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + // A fábrica só será executada e o serviço só será instanciado na linha abaixo! + LService := TMyService(Req.Services.Resolve(TMyService)); + Res.Send(LService.GetMessage); + end); + + THorse.Listen(9000); +end. +``` + +--- + +## 📈 Benefícios Arquiteturais + +1. **Ciclo de Vida Determinístico e Automático:** Garante o descarte seguro de recursos ao término do pipeline HTTP da requisição ativa, eliminando memory leaks. +2. **Isolamento de Estado Concorrente:** Totalmente thread-safe, permitindo que cada thread/request trate suas instâncias de serviços de forma isolada, evitando race conditions. +3. **Lazy Initialization nativa:** Redução no consumo de RAM e no tempo de inicialização de recursos pesados por meio do `AddFactory`, carregando somente o que é realmente demandado pela rota executada. diff --git a/doc/index.md b/doc/index.md index 80f73e90..d2efe5d1 100644 --- a/doc/index.md +++ b/doc/index.md @@ -42,6 +42,7 @@ graph TD | [Request & Response](./request-response.md) | `THorseRequest` (body, params, query, headers, cookies, sessions, multipart). `THorseResponse` (`Send`, `Status`, `ContentType`, `AddHeader`, `RedirectTo`, `SendFile`, `Download`, `RawWebResponse`). | | [Middleware](./middleware.md) | The `Next` proc model; built-in vs custom; registration order; per-route vs global. | | [Lifecycle Hooks](./lifecycle-hooks.md) | Request lifecycle hooks (`onRequest`, `preParsing`, `preValidation`, `onSend`, `onResponse`) to extend and intercept request/response pipelines. | +| [Dependency Injection](./dependency-injection.md) | Lifecycle management and IoC on request scope (direct and lazy injectors). | | [Graceful Shutdown](./graceful-shutdown.md) | Coordinated connection shutdown in production environments (cloud/Kubernetes); telemetry properties `ActiveRequests` and flag `IsShuttingDown`. | | [Writing a Middleware](./writing-middleware.md) | Authoring a production-quality middleware: skeleton, configuration patterns, thread safety, Provider-neutral coding, cross-compiler pitfalls, testing matrix, Boss packaging, publishing. | | [Providers & Application types](./providers.md) | The two-axis model: **Provider** (transport — Indy default; CrossSocket, mORMot2, ICS optional; HttpSys, epoll and IOCP built-in) × **Application type** (Console / VCL / Daemon / LCL / HTTPApplication, plus host-managed Apache / ISAPI / CGI / FCGI). Compatibility matrix and selection guidance. | @@ -62,6 +63,7 @@ doc/ ├── request-response.md ← THorseRequest and THorseResponse API ├── middleware.md ← chaining handlers ├── lifecycle-hooks.md ← request lifecycle hooks (onRequest, etc.) +├── dependency-injection.md ← contextual dependency injection on request scope ├── graceful-shutdown.md ← graceful connection shutdown in production ├── providers.md ← choosing a transport ├── iocp.md ← Windows async I/O completion ports diff --git a/doc/index.pt-BR.md b/doc/index.pt-BR.md index d29c6659..6a03d9e2 100644 --- a/doc/index.pt-BR.md +++ b/doc/index.pt-BR.md @@ -42,6 +42,7 @@ graph TD | [Request e Response](./request-response.pt-BR.md) | `THorseRequest` (body, params, query, headers, cookies, sessions, multipart). `THorseResponse` (`Send`, `Status`, `ContentType`, `AddHeader`, `RedirectTo`, `SendFile`, `Download`, `RawWebResponse`). | | [Middleware](./middleware.pt-BR.md) | O modelo `Next` proc; built-in vs custom; ordem de registro; por-rota vs global. | | [Ganchos de Ciclo de Vida](./lifecycle-hooks.pt-BR.md) | Ganchos de ciclo de vida (`onRequest`, `preParsing`, `preValidation`, `onSend`, `onResponse`) para estender e interceptar requisições. | +| [Injeção de Dependência](./dependency-injection.pt-BR.md) | Gerenciamento de ciclo de vida e IoC no request scope (injetores direct e lazy). | | [Desligamento Suave](./graceful-shutdown.pt-BR.md) | Encerramento coordenado de conexões em ambientes produtivos (nuvem/Kubernetes); propriedades de telemetria `ActiveRequests` e flag `IsShuttingDown`. | | [Criando um Middleware](./writing-middleware.pt-BR.md) | Criando um middleware de qualidade de produção: esqueleto, padrões de configuração, thread safety, código neutro a Provider, armadilhas entre compiladores, matriz de testes, empacotamento Boss, publicação. | | [Providers e Tipos de aplicação](./providers.pt-BR.md) | O modelo de dois eixos: **Provider** (transporte — Indy padrão; CrossSocket, mORMot2, ICS opcionais; HttpSys, epoll e IOCP embutidos) × **Tipo de aplicação** (Console / VCL / Daemon / LCL / HTTPApplication, mais host-managed Apache / ISAPI / CGI / FCGI). Matriz de compatibilidade e guia de escolha. | @@ -62,6 +63,7 @@ doc/ ├── request-response.*.md ← API de THorseRequest e THorseResponse ├── middleware.*.md ← encadeamento de handlers ├── lifecycle-hooks.*.md ← ganchos de ciclo de vida (onRequest, etc.) +├── dependency-injection.*.md ← injeção de dependências no request scope ├── graceful-shutdown.*.md ← desligamento suave em produção ├── providers.*.md ← escolha de transporte ├── iocp.*.md ← portas de conclusão assíncronas (Windows) diff --git a/doc/roadmap/README.md b/doc/roadmap/README.md index 2291a40e..0544d1fd 100644 --- a/doc/roadmap/README.md +++ b/doc/roadmap/README.md @@ -36,10 +36,6 @@ Este documento detalha o planejamento de melhorias arquiteturais de longo prazo ### 8. Roteamento Avançado (Regex e Parâmetros Opcionais) * **Descrição:** Permitir parâmetros opcionais (`/users/:id?`) e restrições de rotas baseadas em Expressões Regulares (`/users/:id(\d+)`) na árvore do Radix Router. -### 10. Injeção de Dependência Contextual (*Request Scope / Context*) -* **Descrição:** Prover um mecanismo estruturado para gerência de dependências cujo ciclo de vida está acoplado ao ciclo da requisição (ex: uma transação de banco de dados ou conexão FireDAC ativa). -* **Ganhos:** - * Facilidade na gerência de concorrência com encerramento e liberação automática de recursos após o fim da requisição. ## ✅ Evolução Arquitetural Entregue (Concluído) @@ -72,6 +68,10 @@ Este documento detalha o planejamento de melhorias arquiteturais de longo prazo * **Status:** 🟢 **Concluído e Liberado** * **Implementação:** Desenvolvido o mecanismo de encerramento coordenado no Core e Provedor de Console (Indy), permitindo interromper novas escutas físicas do socket enquanto as requisições ativas (`ActiveRequests`) são concluídas de forma suave sob um timeout de segurança, expondo as propriedades de telemetria `ActiveRequests` e `IsShuttingDown` (sinalização para Kubernetes/Load Balancers). +### 8. Injeção de Dependência Contextual (Request Scope) +* **Status:** 🟢 **Concluído e Liberado** +* **Implementação:** Desenvolvida a propriedade de ciclo de vida `Services` na classe `THorseRequest`, provendo um container de inversão de controle (IoC) thread-safe que permite injeção direta de instâncias e carregamento preguiçoso (lazy loading) via fábricas com descarte físico e destruição automáticos e determinísticos ao término do pipeline HTTP da requisição ativa. + --- ## ✅ Entregas Recentes de Testes & CI/CD (Concluído) diff --git a/doc/roadmap/prioritization_matrix.md b/doc/roadmap/prioritization_matrix.md index 6ab424c1..7d9702db 100644 --- a/doc/roadmap/prioritization_matrix.md +++ b/doc/roadmap/prioritization_matrix.md @@ -19,7 +19,7 @@ Esta tabela classifica as 13 melhorias pendentes do roadmap técnico do Horse co | 13 | **Static File Server com Range/Cache** | DX / Recursos | 4 | 3 | **1.33** | ➕ **Novo Middleware** (Opcional) | 🟢 **Concluído** (Implementado e Liberado) | | 9 | **Ganchos de Ciclo de Vida (Hooks)** | Ecossistema | 4 | 3 | **1.33** | ➕ **Novo Recurso** (Opcional) | 🟢 **Concluído** (Implementado e Liberado) | | 4 | **Desligamento Suave (Graceful)** | Resiliência | 4 | 3 | **1.33** | ➕ **Novo Recurso** (Opcional) | 🟢 **Concluído** (Implementado e Liberado) | -| 10 | **Injeção de Dependência Contextual** | Arquitetura | 4 | 3 | **1.33** | ➕ **Novo Recurso** (Opcional) | 📅 **Planejar execução** (Gerência de banco) | +| 10 | **Injeção de Dependência Contextual** | Arquitetura | 4 | 3 | **1.33** | ➕ **Novo Recurso** (Opcional) | 🟢 **Concluído** (Implementado e Liberado) | | 1 | **Refatoração Multi-Instance** | Arquitetura | 5 | 4 | **1.25** | ⚙️ **Transparente** (Mantém retrocompatibilidade) | 🎯 **Projeto Estratégico** (Exige refatoração global) | | 6 | **DTO Auto-Binding & Validação** | DX / Produtividade | 5 | 4 | **1.25** | ➕ **Novo Recurso** (Opcional) | 🎯 **Projeto Estratégico** (Uso avançado de RTTI/Atributos) | | 2 | **Pool de Buffers (MemoryBufferPool)** | Otimização | 4 | 4 | **1.00** | ⚙️ **Transparente** (Performance por baixo dos panos) | 🎯 **Projeto Estratégico** (Exige refatoração nos sockets) | diff --git a/doc/skills/README.md b/doc/skills/README.md index d5f42adf..9141db55 100644 --- a/doc/skills/README.md +++ b/doc/skills/README.md @@ -29,6 +29,7 @@ | **horse-zero-allocation** | [`horse-zero-allocation/SKILL.md`](./horse-zero-allocation/SKILL.md) | Coding for Zero-Allocation, using stack buffers, string slices, object pooling, and avoiding thread lock contention. | | **horse-mvc-architecture** | [`horse-mvc-architecture/SKILL.md`](./horse-mvc-architecture/SKILL.md) | Structuring corporate APIs using Clean MVC principles, separating transport from business logic. | | **horse-minimal-api** | [`horse-minimal-api/SKILL.md`](./horse-minimal-api/SKILL.md) | Building rapid, low-boilerplate microservices and mock APIs inside single-file bootstrap models. | +| **horse-dependency-injection** | [`horse-dependency-injection/SKILL.md`](./horse-dependency-injection/SKILL.md) | Managing request-scoped contextual services and IoC (dependency injection) in Delphi and Lazarus. | --- @@ -37,6 +38,6 @@ 1. **Middlewares Order**: The registration order is **critical**. Middlewares like `CORS` and `Jhonson` must be registered **before** defining any routes. 2. **Johnson Memory Management**: The Johnson middleware takes ownership of any `TJSONObject` or `TJSONArray` sent via `Res.Send`. **NEVER** call `.Free` on a JSON object after sending it through `Res.Send` when Johnson is active. 3. **Route Parameters**: Parameters are defined using the colon syntax (e.g., `/products/:id`), not curly braces (e.g., `/products/{id}`). -4. **Thread-Safety**: Horse is inherently multithreaded. **NEVER** share physical database connections (`TFDConnection` or query components) globally across requests. Every route handler must instantiate its own database connection (preferably using connection pooling) or protect shared resources using locks (`TCriticalSection`). +4. **Thread-Safety & IoC**: Horse is inherently multithreaded. **NEVER** share physical database connections (`TFDConnection`) or variables globally across requests. Use the request-scoped `Services` property (IoC) to register and resolve connection factories and scoped services to automate their lifecycle and avoid race conditions or RAM leaks. 5. **Console Output**: Call `SetConsoleCharSet` in console mode endpoints if necessary to prevent character encoding issues. 6. **Stream Management**: The `THorseResponse` object takes ownership of any stream passed to `SendFile`, `Download`, or `Render`. **NEVER** call `.Free` or `FreeAndNil` on a stream after sending it via these response methods. diff --git a/doc/skills/README.pt-BR.md b/doc/skills/README.pt-BR.md index 5b7b560f..324ed6f9 100644 --- a/doc/skills/README.pt-BR.md +++ b/doc/skills/README.pt-BR.md @@ -25,6 +25,7 @@ | **horse-zero-allocation** | [`horse-zero-allocation/SKILL.md`](./horse-zero-allocation/SKILL.md) | Técnicas avançadas de programação sem alocação (Zero-Allocation), fatiamento de string (slices), stack buffers e pools. | | **horse-mvc-architecture** | [`horse-mvc-architecture/SKILL.md`](./horse-mvc-architecture/SKILL.md) | Organização de APIs de grande porte no padrão MVC limpo, separando a camada HTTP de regras de negócio e persistência. | | **horse-minimal-api** | [`horse-minimal-api/SKILL.md`](./horse-minimal-api/SKILL.md) | Desenvolvimento rápido de microsserviços focados, mocks e APIs de arquivo único estruturadas com baixo boilerplate. | +| **horse-dependency-injection** | [`horse-dependency-injection/SKILL.md`](./horse-dependency-injection/SKILL.md) | Gerenciamento de ciclo de vida e IoC no request scope (injeção de dependência) em Delphi e Lazarus. | --- @@ -33,6 +34,6 @@ 1. **Ordem dos Middlewares**: A ordem de registro é **crítica**. Middlewares globais como `CORS` e `Jhonson` devem ser registrados **antes** de definir qualquer rota. 2. **Gerenciamento de Memória (Johnson)**: O middleware Johnson assume a propriedade de qualquer `TJSONObject` ou `TJSONArray` enviado via `Res.Send`. **NUNCA** chame `.Free` ou `FreeAndNil` em um objeto JSON após enviá-lo pelo response caso o Johnson esteja ativo. 3. **Parâmetros de Rota**: Os parâmetros são definidos usando a sintaxe de dois pontos (ex: `/products/:id`), e não chaves (ex: `/products/{id}`). -4. **Thread-Safety**: O Horse é multithreaded por natureza. **NUNCA** compartilhe conexões de banco de dados globais (como `TFDConnection`) entre requisições concorrentes. Cada handler de rota deve obter/criar sua própria conexão (preferencialmente de um Connection Pool). +4. **Thread-Safety e IoC**: O Horse é multithreaded por natureza. **NUNCA** compartilhe conexões de banco de dados (`TFDConnection`) ou variáveis globais entre requisições concorrentes. Use a propriedade `Services` (IoC) no escopo de request para registrar e resolver fábricas de conexões e serviços contextuais, automatizando o ciclo de vida deles e evitando race conditions ou vazamentos de RAM. 5. **Console Output**: Chame `SetConsoleCharSet` em programas modo console para prevenir erros de codificação de caracteres se necessário. 6. **Gerenciamento de Streams**: O objeto `THorseResponse` assume a propriedade de qualquer stream passado para `SendFile`, `Download` ou `Render`. **NUNCA** chame `.Free` ou `FreeAndNil` em um stream após enviá-lo por estes métodos. diff --git a/doc/skills/horse-database-pooling/SKILL.md b/doc/skills/horse-database-pooling/SKILL.md index 02300bcd..4ea2d368 100644 --- a/doc/skills/horse-database-pooling/SKILL.md +++ b/doc/skills/horse-database-pooling/SKILL.md @@ -109,6 +109,43 @@ begin LConnection.Free; end; end; + +--- + +## 3.5. Automated Connection Management via Request Services (IoC) +With Horse's native Request Scope IoC container, you can register a lazy factory for database connections. This ensures the connection is only opened if/when a route resolves it, and guarantees it is freed automatically when the request ends. + +```pascal +// Setup connection factory inside a global middleware +THorse.Use(procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + begin + Req.Services.AddFactory(TFDConnection, + function: TObject + begin + Result := TFDConnection.Create(nil); + TFDConnection(Result).ConnectionDefName := 'MyPooledPGDef'; + TFDConnection(Result).Connected := True; + end); + Next(); + end); + +// Usage in Route Handler (No manual connection free required!) +THorse.Get('/users', procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LConnection: TFDConnection; + LQuery: TFDQuery; + begin + LConnection := TFDConnection(Req.Services.Resolve(TFDConnection)); + LQuery := TFDQuery.Create(nil); + try + LQuery.Connection := LConnection; + LQuery.SQL.Text := 'SELECT id, name FROM users'; + LQuery.Open; + Res.Send(LQuery.ToJSONArray); + finally + LQuery.Free; + end; // LConnection is freed automatically by Horse when the request finishes! + end); ``` --- diff --git a/doc/skills/horse-dependency-injection/SKILL.md b/doc/skills/horse-dependency-injection/SKILL.md new file mode 100644 index 00000000..f8ac42f5 --- /dev/null +++ b/doc/skills/horse-dependency-injection/SKILL.md @@ -0,0 +1,89 @@ +--- +name: horse-dependency-injection +description: Guide for managing request-scoped contextual services and IoC (dependency injection) in Delphi and Lazarus. +--- + +# Horse Contextual Dependency Injection (Request Scope) + +## 1. Request Scope Architecture +The `Services` property in `THorseRequest` provides a thread-safe, request-scoped Inversion of Control (IoC) container. This container manages the lifecycle of classes and objects that need to exist only during the execution of a single HTTP request (e.g., database connections, repository patterns, user context). + +* **Ownership**: The container takes ownership (`doOwnsValues := True`) of registered instances. +* **Automatic Disposal**: All registered service instances are automatically destroyed when the request finishes, eliminating memory leaks and removing the need for manual `try/finally` blocks inside route handlers. + +--- + +## 2. Direct Instance Injection (`Add`) +Use `Add` to register an already instantiated object. The container takes ownership and will destroy the instance when the request ends. + +```pascal +procedure SetContextMiddleware(Req: THorseRequest; Res: THorseResponse; Next: TProc); +var + LUserContext: TUserContext; +begin + LUserContext := TUserContext.Create; + LUserContext.UserId := Req.Headers['X-User-ID']; + + // Register the instance + Req.Services.Add(TUserContext, LUserContext); + Next(); +end; + +procedure GetProfileHandler(Req: THorseRequest; Res: THorseResponse; Next: TProc); +var + LUserContext: TUserContext; +begin + // Resolve and use the registered service + LUserContext := TUserContext(Req.Services.Resolve(TUserContext)); + Res.Send('Profile data for user: ' + LUserContext.UserId); +end; +``` + +--- + +## 3. Lazy Factory Injection (`AddFactory`) +Use `AddFactory` to register a factory delegate. The object will only be instantiated at the exact moment `Resolve` is called (*Lazy Loading*). Once instantiated, it is cached for subsequent resolves in the same request and destroyed at the end. + +This is highly recommended for heavy resources (like database connections) that might not be needed in every execution path of a route. + +```pascal +procedure RegisterDatabaseMiddleware(Req: THorseRequest; Res: THorseResponse; Next: TProc); +begin + // Register the factory method + Req.Services.AddFactory(TFDConnection, + function: TObject + begin + Result := TFDConnection.Create(nil); + TFDConnection(Result).ConnectionDefName := 'PooledPGDef'; + TFDConnection(Result).Connected := True; + end); + Next(); +end; + +procedure QueryUsersHandler(Req: THorseRequest; Res: THorseResponse; Next: TProc); +var + LConnection: TFDConnection; + LQuery: TFDQuery; +begin + // The connection is physically created and opened only on the next line! + LConnection := TFDConnection(Req.Services.Resolve(TFDConnection)); + + LQuery := TFDQuery.Create(nil); + try + LQuery.Connection := LConnection; + LQuery.SQL.Text := 'SELECT id, name FROM users'; + LQuery.Open; + Res.Send(LQuery.ToJSONArray); + finally + LQuery.Free; + end; // LConnection is automatically freed by the container when the request ends! +end; +``` + +--- + +## 4. Best Practices for Dependency Injection in Horse +1. **Avoid Global Resolving**: Never resolve services outside the active request flow. The `Services` container belongs exclusively to the thread handling the current `THorseRequest`. +2. **Cast Returned Objects**: The `Resolve` method returns a raw `TObject` to preserve compatibility across old Delphi versions. You must cast it to your concrete class (e.g., `TMyService(Req.Services.Resolve(TMyService))`). +3. **One Registry Per Class Type**: Only one instance or factory can be registered per class type in the same request. Registering a class that is already registered will raise an exception. +4. **No Manual Frees**: Do not call `.Free` or `FreeAndNil` on objects resolved from `Req.Services` that you want to persist until the end of the request. diff --git a/samples/delphi/console_dependency_injection/ConsoleDependencyInjection.dpr b/samples/delphi/console_dependency_injection/ConsoleDependencyInjection.dpr new file mode 100644 index 00000000..2a2fe1b9 --- /dev/null +++ b/samples/delphi/console_dependency_injection/ConsoleDependencyInjection.dpr @@ -0,0 +1,87 @@ +program ConsoleDependencyInjection; + +{$APPTYPE CONSOLE} + +{$R *.res} + +uses + Horse, + Horse.Commons, + System.SysUtils; + +type + TMyService = class + private + FId: string; + public + constructor Create(const AId: string); + destructor Destroy; override; + function GetMessage: string; + end; + +{ TMyService } + +constructor TMyService.Create(const AId: string); +begin + inherited Create; + FId := AId; + Writeln(Format('[TMyService] Instanciado com ID: %s', [FId])); +end; + +destructor TMyService.Destroy; +begin + Writeln(Format('[TMyService] Destruído com ID: %s (Escopo limpo)', [FId])); + inherited Destroy; +end; + +function TMyService.GetMessage: string; +begin + Result := 'Olá de um Serviço Contextual Injetado! ID: ' + FId; +end; + +begin + THorse.Get('/resolve', + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + // Cria e injeta o serviço no escopo do Request. + // O Horse assumirá a posse do objeto e o destruirá automaticamente ao final do request. + LService := TMyService.Create('InstanciaDireta'); + Req.Services.Add(TMyService, LService); + Next(); + end, + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + // Resolve o serviço injetado + LService := TMyService(Req.Services.Resolve(TMyService)); + Res.Send(LService.GetMessage); + end); + + THorse.Get('/lazy', + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + begin + // Registra a fábrica sem instanciar o objeto ainda (Lazy Loading) + Req.Services.AddFactory(TMyService, + function: TObject + begin + Result := TMyService.Create('LazyFactory'); + end); + Next(); + end, + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + Writeln('-> Resolvendo o serviço lazy...'); + // O objeto só será criado agora, no momento do Resolve! + LService := TMyService(Req.Services.Resolve(TMyService)); + Res.Send(LService.GetMessage); + end); + + Writeln('Servidor Horse rodando na porta 9000 com Injeção de Dependências...'); + Writeln('Acesse /resolve ou /lazy'); + THorse.Listen(9000); +end. diff --git a/samples/lazarus/console_dependency_injection/ConsoleDependencyInjection.lpr b/samples/lazarus/console_dependency_injection/ConsoleDependencyInjection.lpr new file mode 100644 index 00000000..f1fa3d9d --- /dev/null +++ b/samples/lazarus/console_dependency_injection/ConsoleDependencyInjection.lpr @@ -0,0 +1,80 @@ +program ConsoleDependencyInjection; + +{$MODE DELPHI}{$H+} + +uses + {$IFDEF UNIX} + cthreads, + {$ENDIF} + Horse, + Horse.Commons, + SysUtils; + +type + TMyService = class + private + FId: string; + public + constructor Create(const AId: string); + destructor Destroy; override; + function GetMessage: string; + end; + +{ TMyService } + +constructor TMyService.Create(const AId: string); +begin + inherited Create; + FId := AId; + Writeln(Format('[TMyService] Instanciado com ID: %s', [FId])); +end; + +destructor TMyService.Destroy; +begin + Writeln(Format('[TMyService] Destruído com ID: %s (Escopo limpo)', [FId])); + inherited Destroy; +end; + +function TMyService.GetMessage: string; +begin + Result := 'Olá de um Serviço Contextual Injetado! ID: ' + FId; +end; + +begin + THorse.Get('/resolve', [ + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + LService := TMyService.Create('InstanciaDireta'); + Req.Services.Add(TMyService, LService); + Next(); + end, + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + LService := TMyService(Req.Services.Resolve(TMyService)); + Res.Send(LService.GetMessage); + end]); + + THorse.Get('/lazy', + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TMyService; + begin + Req.Services.AddFactory(TMyService, + function: TObject + begin + Result := TMyService.Create('LazyFactory'); + end); + + Writeln('-> Resolvendo o serviço lazy...'); + LService := TMyService(Req.Services.Resolve(TMyService)); + Res.Send(LService.GetMessage); + end); + + Writeln('Servidor Horse rodando na porta 9000 com Injeção de Dependências...'); + Writeln('Acesse /resolve ou /lazy'); + THorse.Listen(9000); +end. diff --git a/src/Horse.Commons.pas b/src/Horse.Commons.pas index 4c4964ee..e7f4a919 100644 --- a/src/Horse.Commons.pas +++ b/src/Horse.Commons.pas @@ -21,6 +21,9 @@ interface type TMethodType = (mtAny, mtGet, mtPut, mtPost, mtHead, mtDelete, mtPatch, mtQuery); + THorseServiceFactory = {$IF DEFINED(FPC)}function: T{$ELSE}reference to function: T{$ENDIF}; + THorseServiceFactory = {$IF DEFINED(FPC)}function: TObject{$ELSE}reference to function: TObject{$ENDIF}; + {$SCOPEDENUMS ON} THTTPStatus = ( Continue = 100, diff --git a/src/Horse.Core.Context.pas b/src/Horse.Core.Context.pas index 21a93672..237b0ae9 100644 --- a/src/Horse.Core.Context.pas +++ b/src/Horse.Core.Context.pas @@ -8,143 +8,131 @@ interface uses {$IF DEFINED(FPC)} + SysUtils, Generics.Collections, - SyncObjs, {$ELSE} + System.SysUtils, System.Generics.Collections, - System.SyncObjs, {$ENDIF} Horse.Commons; type - THorseContext = class + THorseServiceFactoryWrapper = class private - FRequest: TObject; - FResponse: TObject; - FArena: TObject; + FFactory: THorseServiceFactory; public - constructor Create; - destructor Destroy; override; - procedure Reset; - property Request: TObject read FRequest; - property Response: TObject read FResponse; - property Arena: TObject read FArena; + constructor Create(const AFactory: THorseServiceFactory); + function CreateInstance: TObject; end; - THorseContextPool = class + THorseRequestContext = class private - FList: TQueue; - FLock: TCriticalSection; - FMaxCount: Integer; - FAllocatedCount: Integer; - class var FInstance: THorseContextPool; + FInstances: TObjectDictionary; + FUnownedInstances: TDictionary; + FFactories: TObjectDictionary; + function GetKey(const AClass: TClass): string; public - constructor Create(const AMaxCount: Integer = 1024); + constructor Create; destructor Destroy; override; - function Acquire: THorseContext; - procedure Release(const AContext: THorseContext); - procedure WarmUp(const ACount: Integer); - class property Instance: THorseContextPool read FInstance; + + procedure Add(const AClass: TClass; const AInstance: TObject; const AOwns: Boolean = True); + procedure AddFactory(const AClass: TClass; const AFactory: THorseServiceFactory); + + function Resolve(const AClass: TClass): TObject; end; implementation -uses - Horse.Request, - Horse.Response; - -{ THorseContext } +{ THorseServiceFactoryWrapper } -constructor THorseContext.Create; +constructor THorseServiceFactoryWrapper.Create(const AFactory: THorseServiceFactory); begin inherited Create; - FRequest := THorseRequest.Create; - FResponse := THorseResponse.Create(nil); - FArena := THorseArenaAllocator.Create(65536); - THorseRequest(FRequest).Arena := THorseArenaAllocator(FArena); -end; - -destructor THorseContext.Destroy; -begin - FRequest.Free; - FResponse.Free; - FArena.Free; - inherited; + FFactory := AFactory; end; -procedure THorseContext.Reset; +function THorseServiceFactoryWrapper.CreateInstance: TObject; begin - THorseRequest(FRequest).Clear; - THorseResponse(FResponse).Clear; - THorseArenaAllocator(FArena).Reset; + Result := FFactory(); end; -{ THorseContextPool } +{ THorseRequestContext } -constructor THorseContextPool.Create(const AMaxCount: Integer); +constructor THorseRequestContext.Create; begin inherited Create; - FList := TQueue.Create; - FLock := TCriticalSection.Create; - FMaxCount := AMaxCount; - FAllocatedCount := 0; + FInstances := TObjectDictionary.Create([doOwnsValues]); + FUnownedInstances := TDictionary.Create; + FFactories := TObjectDictionary.Create([doOwnsValues]); end; -destructor THorseContextPool.Destroy; -var - LContext: THorseContext; +destructor THorseRequestContext.Destroy; begin - FLock.Enter; - try - while FList.Count > 0 do - begin - LContext := FList.Dequeue; - LContext.Free; - end; - FList.Free; - finally - FLock.Leave; - end; - FLock.Free; - inherited; + FFactories.Free; + FUnownedInstances.Free; + FInstances.Free; + inherited Destroy; end; -function THorseContextPool.Acquire: THorseContext; +function THorseRequestContext.GetKey(const AClass: TClass): string; begin - Result := THorseContext.Create; + if Assigned(AClass) then + Result := AClass.ClassName + else + Result := 'UnknownClass'; end; -procedure THorseContextPool.Release(const AContext: THorseContext); +procedure THorseRequestContext.Add(const AClass: TClass; const AInstance: TObject; const AOwns: Boolean); +var + LKey: string; begin - if AContext <> nil then - AContext.Free; + LKey := GetKey(AClass); + + if FInstances.ContainsKey(LKey) then + FInstances.Remove(LKey); + if FUnownedInstances.ContainsKey(LKey) then + FUnownedInstances.Remove(LKey); + + if AOwns then + FInstances.Add(LKey, AInstance) + else + FUnownedInstances.Add(LKey, AInstance); end; -procedure THorseContextPool.WarmUp(const ACount: Integer); +procedure THorseRequestContext.AddFactory(const AClass: TClass; const AFactory: THorseServiceFactory); var - I: Integer; - LContext: THorseContext; + LKey: string; + LWrapper: THorseServiceFactoryWrapper; begin - FLock.Enter; - try - for I := 1 to ACount do - begin - if FAllocatedCount < FMaxCount then - begin - LContext := THorseContext.Create; - FList.Enqueue(LContext); - Inc(FAllocatedCount); - end; - end; - finally - FLock.Leave; - end; + LKey := GetKey(AClass); + if FFactories.TryGetValue(LKey, LWrapper) then + FFactories.Remove(LKey); + FFactories.Add(LKey, THorseServiceFactoryWrapper.Create(AFactory)); end; -initialization - THorseContextPool.FInstance := THorseContextPool.Create(1024); +function THorseRequestContext.Resolve(const AClass: TClass): TObject; +var + LKey: string; + LVal: TObject; + LWrapper: THorseServiceFactoryWrapper; +begin + LKey := GetKey(AClass); + + if FInstances.TryGetValue(LKey, LVal) then + Exit(LVal); + + if FUnownedInstances.TryGetValue(LKey, LVal) then + Exit(LVal); + + if FFactories.TryGetValue(LKey, LWrapper) then + begin + LVal := LWrapper.CreateInstance; + FInstances.Add(LKey, LVal); + FFactories.Remove(LKey); + Exit(LVal); + end; -finalization - THorseContextPool.FInstance.Free; + Result := nil; +end; end. diff --git a/src/Horse.Request.pas b/src/Horse.Request.pas index 693bfb04..d692c5fd 100644 --- a/src/Horse.Request.pas +++ b/src/Horse.Request.pas @@ -22,7 +22,8 @@ interface {$ENDIF} Horse.Core.Param, Horse.Session, - Horse.Commons; + Horse.Commons, + Horse.Core.Context; type THorseRequest = class @@ -40,6 +41,7 @@ THorseRequest = class FSessions: THorseSessions; FArena: THorseArenaAllocator; FOwnsArena: Boolean; + FServices: THorseRequestContext; { =========================================================================== PATCH-REQ-3 CrossSocket shadow fields (populated by Populate, nil by default) =========================================================================== } @@ -91,6 +93,7 @@ THorseRequest = class function IsMultipartForm: Boolean; function IsFormURLEncoded: Boolean; function CanLoadContentFields: Boolean; + function GetServices: THorseRequestContext; public function Body: string; overload; virtual; function Body: T; overload; @@ -212,6 +215,7 @@ THorseRequest = class { =========================================================================== } property MatchedRoute: string read FMatchedRoute write FMatchedRoute; property State: TObjectDictionary read FState; + property Services: THorseRequestContext read GetServices; destructor Destroy; override; end; @@ -285,6 +289,13 @@ function THorseRequest.Cookie: THorseCoreParam; Result := FCookie; end; +function THorseRequest.GetServices: THorseRequestContext; +begin + if not Assigned(FServices) then + FServices := THorseRequestContext.Create; + Result := FServices; +end; + constructor THorseRequest.Create(const AWebRequest: {$IF DEFINED(FPC)}TRequest{$ELSE}TWebRequest{$ENDIF}); begin FWebRequest := AWebRequest; @@ -379,6 +390,8 @@ procedure THorseRequest.Clear; FMatchedRoute := ''; if Assigned(FState) then FState.Clear; + if Assigned(FServices) then + FreeAndNil(FServices); end; { =========================================================================== } @@ -408,6 +421,8 @@ destructor THorseRequest.Destroy; { end PATCH-REQ-8 } if FOwnsArena and Assigned(FArena) then FreeAndNil(FArena); + if Assigned(FServices) then + FreeAndNil(FServices); if Assigned(FState) then FreeAndNil(FState); inherited; diff --git a/tests/src/Console.dpr b/tests/src/Console.dpr index b93983d7..d1f8d6e6 100644 --- a/tests/src/Console.dpr +++ b/tests/src/Console.dpr @@ -81,6 +81,7 @@ uses Tests.Integration.Query in 'tests\Tests.Integration.Query.pas', Tests.Integration.LifecycleHooks in 'tests\Tests.Integration.LifecycleHooks.pas', Tests.Integration.GracefulShutdown in 'tests\Tests.Integration.GracefulShutdown.pas', + Tests.Integration.DependencyInjection in 'tests\Tests.Integration.DependencyInjection.pas', Horse.Mime in '..\..\src\Horse.Mime.pas', Horse.Utils in '..\..\src\Horse.Utils.pas', Horse.Provider.Config in '..\..\src\Horse.Provider.Config.pas', diff --git a/tests/src/HorseTestDefines.inc b/tests/src/HorseTestDefines.inc index 2a2650f6..93c0390f 100644 --- a/tests/src/HorseTestDefines.inc +++ b/tests/src/HorseTestDefines.inc @@ -1,2 +1 @@ {$DEFINE CI} -{$DEFINE HORSE_PROVIDER_IOCP} diff --git a/tests/src/tests/Tests.Integration.DependencyInjection.pas b/tests/src/tests/Tests.Integration.DependencyInjection.pas new file mode 100644 index 00000000..8eeda400 --- /dev/null +++ b/tests/src/tests/Tests.Integration.DependencyInjection.pas @@ -0,0 +1,191 @@ +unit Tests.Integration.DependencyInjection; + +interface + +uses + DUnitX.TestFramework, Horse, Horse.Commons, Horse.Core.Context, + {$IF DEFINED(FPC)} + SyncObjs, + {$ELSE} + System.SyncObjs, + {$ENDIF} + System.SysUtils, System.Classes, System.Threading, System.Net.HttpClient, Tests.CleanupHelper; + +type + TTestService = class + private + FValue: string; + class var FActiveCount: Integer; + class function GetActiveCount: Integer; static; + class procedure SetActiveCount(const AValue: Integer); static; + public + constructor Create; + destructor Destroy; override; + property Value: string read FValue write FValue; + class property ActiveCount: Integer read GetActiveCount write SetActiveCount; + end; + + [TestFixture] + TTestIntegrationDependencyInjection = class + private + const TEST_PORT = 9099; + public + [SetupFixture] + procedure SetupFixture; + [TearDownFixture] + procedure TearDownFixture; + + [Test] + procedure TestDirectInstanceInjection; + + [Test] + procedure TestLazyFactoryInjectionAndCleanup; + end; + +implementation + +{ TTestService } + +class function TTestService.GetActiveCount: Integer; +begin + Result := FActiveCount; +end; + +class procedure TTestService.SetActiveCount(const AValue: Integer); +begin + FActiveCount := AValue; +end; + +constructor TTestService.Create; +begin + inherited Create; + TInterlocked.Increment(FActiveCount); + FValue := 'default-value'; +end; + +destructor TTestService.Destroy; +begin + TInterlocked.Decrement(FActiveCount); + inherited Destroy; +end; + +{ TTestIntegrationDependencyInjection } + +procedure TTestIntegrationDependencyInjection.SetupFixture; +begin +end; + +procedure TTestIntegrationDependencyInjection.TearDownFixture; +begin + ClearGlobalState; +end; + +procedure TTestIntegrationDependencyInjection.TestDirectInstanceInjection; +var + LServerThread: TThread; + LClient: THTTPClient; + LRes: IHTTPResponse; +begin + ClearGlobalState; + TTestService.ActiveCount := 0; + + THorse.Get('/direct', + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LService: TTestService; + LResolved: TTestService; + begin + LService := TTestService.Create; + LService.Value := 'direct-ok'; + Req.Services.Add(TTestService, LService); + + LResolved := TTestService(Req.Services.Resolve(TTestService)); + Res.Send(LResolved.Value); + end); + + LServerThread := TThread.CreateAnonymousThread( + procedure + begin + THorse.Listen(TEST_PORT); + end); + LServerThread.FreeOnTerminate := False; + LServerThread.Start; + + TThread.Sleep(500); + + LClient := THTTPClient.Create; + try + LRes := LClient.Get(Format('http://localhost:%d/direct', [TEST_PORT])); + Assert.AreEqual(200, LRes.StatusCode); + Assert.AreEqual('direct-ok', LRes.ContentAsString); + finally + LClient.Free; + end; + + THorse.StopListenGraceful(2000); + LServerThread.WaitFor; + LServerThread.Free; + + Assert.AreEqual(0, TTestService.ActiveCount, 'O serviço injetado de forma pertencente deve ter sido destruído'); +end; + +procedure TTestIntegrationDependencyInjection.TestLazyFactoryInjectionAndCleanup; +var + LServerThread: TThread; + LClient: THTTPClient; + LRes: IHTTPResponse; + LFactoryCalled: Boolean; +begin + ClearGlobalState; + TTestService.ActiveCount := 0; + LFactoryCalled := False; + + THorse.Get('/lazy', + procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc) + var + LResolved: TTestService; + begin + Req.Services.AddFactory(TTestService, + function: TObject + begin + LFactoryCalled := True; + Result := TTestService.Create; + TTestService(Result).Value := 'lazy-ok'; + end); + + Assert.IsFalse(LFactoryCalled, 'A fábrica não deve ter sido executada antes do Resolve'); + LResolved := TTestService(Req.Services.Resolve(TTestService)); + Assert.IsTrue(LFactoryCalled, 'A fábrica deve ser executada no momento do Resolve'); + Res.Send(LResolved.Value); + end); + + LServerThread := TThread.CreateAnonymousThread( + procedure + begin + THorse.Listen(TEST_PORT); + end); + LServerThread.FreeOnTerminate := False; + LServerThread.Start; + + TThread.Sleep(500); + + LClient := THTTPClient.Create; + try + LRes := LClient.Get(Format('http://localhost:%d/lazy', [TEST_PORT])); + Assert.AreEqual(200, LRes.StatusCode); + Assert.AreEqual('lazy-ok', LRes.ContentAsString); + finally + LClient.Free; + end; + + THorse.StopListenGraceful(2000); + LServerThread.WaitFor; + LServerThread.Free; + + Assert.AreEqual(0, TTestService.ActiveCount, 'O serviço lazy deve ter sido destruído após o request'); +end; + +initialization + TDUnitX.RegisterTestFixture(TTestIntegrationDependencyInjection); + +end.