Contextual Dependency Injection (Request Scope)
July 10, 2026 · View on GitHub
Read this in English or Português (BR).
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:
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.
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.
Req.Services.AddFactory(TMyService,
function: TObject
begin
Result := TMyService.Create;
end);
💻 Complete Practical Example
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
- Deterministic and Automatic Lifecycle: Ensures resources are safely disposed of at the end of the HTTP routing pipeline, eliminating memory leaks.
- Concurrent State Isolation: Fully thread-safe, allowing each concurrent request to process its service instances in complete isolation, avoiding race conditions.
- Native Lazy Initialization: Reduced RAM footprint and faster request processing overheads using
AddFactory, instantiating services only when required by the executed route.