Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice when a DI service depends on another DI service

Looking to get some advice/information on Blazor best practice when an injected service depends on another service.

I'm going to use the standard Blazor server template provided by Microsoft as an example. I create my test project with dotnet new blazorserver

Suppose my WeatherForecastService class depends on an external data service IDataService for data. My interface is defined as follows:

public interface IDataService
{
    public string GetData();
}

and a concrete class that I'll use as a service is defined as

public class DataService : IDataService
{
    public string GetData()
    {
        //any implementation would communicate with an external service
        return "Some Data Here";
    }
}

To use IDataService in WeatherForecastService I've thought of two ways of using the service.

Option 1 - inject the dependency as part of method definitions

I could inject the dependency into wherever it's needed. For example if I added a GetDataFromDataService method to WeatherForecastService it might look as follows:

public string GetDataFromDataService(IDataService service)
{
    return service.GetData();
}

Benefits

  • Registering the service is easy via Program.fs i.e.

builder.Services.AddSingleton<IDataService>(new DataService());

  • This service is available to other services that might need it.

Drawbacks

  • every method that needs this service needs the service passed in as a parameter (could get messy)
  • every component that requires WeatherForecastService injected will likely need a second service injected as well.

Option 2 - inject the dependency as part of the class constructor

As an alternative, one could inject the service as part of the WeatherForecastService constructor e.g.

private IDataService service { get; }
public WeatherForecastService(IDataService service)
{
    this.service = service;
}
public string GetDataFromDataService()
{
    return service.GetData();
}

Benefits

  • service passed in once. Can be reused several times.

Drawbacks

  • service wouldn't be available for other services
  • depending on how complex a constructor is, you may find yourself doing the following in Program.fs which just feels wrong.
var dataService = new DataService();
builder.Services.AddSingleton(new WeatherForecastService(dataService));

Conclusion

I've listed the above options as they're the ones I've thought of so far - are there any I'm missing? Additionally, is there a best practice around this or is it a case of "it depends"?

Many thanks for any advice on this!

like image 760
Christopher Dunderdale Avatar asked Dec 07 '25 06:12

Christopher Dunderdale


2 Answers

The simple approach is also "best practice"

  • use Option 2, constructor injection.

Drawbacks

  • service wouldn't be available for other services
  • depending on how complex a constructor is, you may find yourself doing the following in Program.fs which just feels wrong.

This shouldn't come up.

  • "available for other services" : they should use their own injection. Don't add coupling you don't need.

  • "... how complex a constructor is" shouldn't matter:

builder.Services.AddSingleton(new
WeatherForecastService(dataService)); ```

This should become

builder.Services.AddTransient<WeatherForecastService>();
builder.Services.AddTransient<IDataService, DataService>();

part of the DI principle is that you don't new services.

like image 124
Henk Holterman Avatar answered Dec 09 '25 19:12

Henk Holterman


I agree with @HH on "good pactice".

However, consider your WeatherForecastService. What scope do you want that service to have?

The consumer of that service is a component: either a page or a form of some type. If you want to match the scope of the service to the consumer you have an issue. Scoped is too wide: it lives for the lifespan of the SPA session. Transient works as long as:

  1. You don't want form sub-components to also use the service.
  2. The services doesn't implement IDisposable/IAsyncDisposable.

If either of the above apply, you need a different solution.

You need to get an instance of WeatherForecastService outside the service container context using the ActivatorUtilities class. This lets you activate an instance of a class outside the context of the servive container, but populated with services from the container. You can even provide additional constructor arguments that are not provided by the container.

Here are a couple of extension methods for IServiceProvider that demonstrate how to use ActivatorUtilities.

public static class ServiceUtilities
{
    public static TService? GetComponentService<TService>
    (this IServiceProvider serviceProvider) where TService : class
    {
        var serviceType = serviceProvider.GetService<TService>()?.GetType();

        if (serviceType is null)
            return ActivatorUtilities.CreateInstance<TService>(serviceProvider);

        return ActivatorUtilities.CreateInstance
               (serviceProvider, serviceType) as TService;
    }

    public static bool TryGetComponentService<TService>
        (this IServiceProvider serviceProvider,[NotNullWhen(true)] 
            out TService? service) where TService : class
    {
        service = serviceProvider.GetComponentService<TService>();
        return service != null;
    }
}

You can then cascade the instance in the form/page and any components that need the service capture the instance as a CascadingParameter. The EditContext/EditForm works this way.

Ensure you dispose the object correctly in the page/form.

References:

The above solution is covered in more detail in a CodeProject article - https://www.codeproject.com/Articles/5352916/Matching-Services-with-the-Blazor-Component-Scope.

SO answer - Using ActivatorUtilities.CreateInstance To Create Instance From Type

like image 20
MrC aka Shaun Curtis Avatar answered Dec 09 '25 18:12

MrC aka Shaun Curtis