davidfowl / DotNetCodingPatterns

A collection of coding patterns in no particular order
1.38k stars 99 forks source link

In what situations we need Lazy initialization of services? #17

Closed ParsaMehdipour closed 4 months ago

ParsaMehdipour commented 4 months ago

In some cases, it might be necessary to create services after the constructor has been called. I would like to know about specific situations where this could occur.

kiapanahi commented 4 months ago

⚠️ This is bad design and you should not replicate it

Consider two services that are wrongly taking dependencies on each other. e.g., VoucherService and DiscountService.

Let's say in our example a method on VoucherService called GenerateDiscountForVoucher needs to call a method on the DiscountService.GenerateDiscountCode() which in returns calls VoucherService.ValidateVoucher() to validate the voucher status (again, this is an awful design). In this situation, your VoucherService injects the DiscountService using the conventional ctor injection mechanism. However the DiscountService now cannot do the same (injecting the VoucherService using ctor injection).

To have a workaround for this scenario, you can use the lazy initialization technique.

Sample code


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<VoucherService>();
//builder.Services.AddScoped<DiscountService>();
builder.Services.AddScoped<DiscountServiceWorkaround>();

var app = builder.Build();

app.MapGet("/", (VoucherService vs) =>
{
    vs.Do(5);
    return Results.Ok();
});

app.Run();

internal sealed class VoucherService(DiscountServiceWorkaround discountService, ILogger<VoucherService> logger)
{

    public void Do(int x)
    {
        logger.LogInformation("voucher:do:before");
        discountService.Do(x);
        logger.LogInformation("voucher:do:after");
    }

    internal void Validate(int x)
    {
        logger.LogInformation("voucher:validate");
    }
}
internal sealed class DiscountService(VoucherService voucherService, ILogger<DiscountService> logger)
{
    public void Do(int x)
    {
        logger.LogInformation("discount:do:before");
        voucherService.Validate(x);
        logger.LogInformation("discount:do:after");

    }
}

internal sealed class DiscountServiceWorkaround(IServiceProvider sp, ILogger<DiscountService> logger)
{
    private VoucherService VoucherService => sp.GetRequiredService<VoucherService>();
    public void Do(int x)
    {
        logger.LogInformation("discount_workaround:do:before");
        VoucherService.Validate(x);
        logger.LogInformation("discount_workaround:do:after");

    }
}
ParsaMehdipour commented 4 months ago

Thank you for your informative answer. In this scenario, the two services are interdependent. Instead of one service calling the other directly, a workaround is implemented by using an intermediary dependent service. And uses the lazy initialization.

What alternative design would you recommend for this situation?

kiapanahi commented 4 months ago

Well right off the bat these two come to mind:

  1. Messaging using a mediator-like pattern.
  2. re-thinking the ownership of the methods and the logic flow fundamentally.
  3. Enrich the DTO passed between services with enough data so that the other service would work independently of the other. i.e., the VouherService in this case, could enrich the object passed to the DiscountService with an attribute like ValidityStatus so that the DiscountService doesn't need the method call altogether.
ParsaMehdipour commented 4 months ago

Thanks I had mediator-like patterns as my solution of choice