Skip to content

Lazy<T> support for deferred initialization #8

@dmytroett

Description

@dmytroett

Description

Add support for Lazy<T> injection, enabling deferred initialization of services. This allows services to be created only when first accessed, which is useful for expensive operations, circular dependency breaking, and performance optimization.

Motivation

Some services are expensive to initialize or create circular dependencies. The Lazy<T> pattern provides a way to defer initialization until the service is actually needed. This is a standard pattern in .NET dependency injection and should be supported automatically.

Usage Scenario

Deferred Initialization for Expensive Services

public interface IReportGenerator
{
    string GenerateReport(string type);
}

[RegisterAs<IReportGenerator>]
[Singleton]
public class ReportGenerator : IReportGenerator
{
    private readonly HeavyReportEngine _engine;

    public ReportGenerator()
    {
        // Expensive initialization - takes 5 seconds
        _engine = new HeavyReportEngine();
    }

    public string GenerateReport(string type) => _engine.Generate(type);
}

public interface IReportService
{
    void ProcessReports();
}

[RegisterAs<IReportService>]
[Scoped]
public class ReportService : IReportService
{
    private readonly Lazy<IReportGenerator> _generator;

    // Lazy<T> is injected instead of IReportGenerator
    public ReportService(Lazy<IReportGenerator> generator)
    {
        _generator = generator;
        // ReportGenerator is NOT created yet
    }

    public void ProcessReports()
    {
        // ReportGenerator is created HERE on first access
        var report = _generator.Value.GenerateReport("daily");
    }
}

Breaking Circular Dependencies

public interface IOrderService { }
public interface IPaymentService { }

[RegisterAs<IOrderService>]
[Scoped]
public class OrderService : IOrderService
{
    // Use Lazy to break circular reference
    public OrderService(Lazy<IPaymentService> paymentService)
    {
        _paymentService = paymentService;
    }
}

[RegisterAs<IPaymentService>]
[Scoped]
public class PaymentService : IPaymentService
{
    // Direct dependency on IOrderService would create circular reference
    public PaymentService(IOrderService orderService)
    {
        _orderService = orderService;
    }
}

Implementation Notes

  • Support Lazy<TService> injection where TService is a registered service
  • The DI container should automatically register Lazy<T> wrappers for all services
  • When Lazy<T> is requested, return a Lazy<T> that delegates to the actual service resolution
  • Ensure lazy services respect their registered lifetime (singleton, scoped, transient)
  • Support with keyed services: Lazy<IService> with a specific key
  • For scoped services wrapped in Lazy<T>, ensure they follow scoping rules properly
  • Add unit tests for:
    • Lazy initialization timing
    • Multiple lazy requests for the same service
    • Keyed lazy services
    • Mixed lifetime scenarios (lazy singleton wrapping scoped, etc.)
  • Add integration test in RegistrationsTestsWithStandardAssemblyName.cs
  • Document that accessing Lazy<T>.Value triggers resolution at that point

Technical Approach

The source generator should:

  1. Detect when Lazy<TService> is used in constructor parameters
  2. Ensure TService is a registered service (or generate error)
  3. For keyed services, support Lazy<TService> with the same key detection
  4. Generate registration code that provides Lazy<T> instances via factory methods

Example generated code pattern:

services.AddScoped(typeof(Lazy<IMyService>), provider => 
    new Lazy<IMyService>(() => provider.GetRequiredService<IMyService>()));

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions