1

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!

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Whether to inject in to the constructor or not is probably opinion based, however I setup needed classes in `Program.cs` as follows for example: `builder.Services.AddSingleton();` I only need to do this once, without any convolution. If another injected class needs this, I just inject it into the constructor of this class. No need any additional newing up like in your example code in option2 – Roland Deschain Feb 23 '23 at 11:42
  • How would you "inject" in Option 1 ? Afaik the DI in ASP.NET doesn't do parameter injection. – H H Feb 23 '23 at 12:34
  • You're asking about DI, not Blazor. .NET Core's DI has no parameter or property injection BUT minimal API and controller actions can have injected parameters with the `[FromService]` attribute. That's possible only because the Controller/API middleware explicitly looks for those attributes when it tries to bind request data to action parameters. You can't use that with any service – Panagiotis Kanavos Feb 23 '23 at 12:58

2 Answers2

3

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.

H H
  • 263,252
  • 30
  • 330
  • 514
0

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

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Is any of this necessary here? I'd like to stress that the vast majority of Services should be Transient, certainly if they don't hold any state, like here. – H H Feb 24 '23 at 11:19
  • @HH In general I agree, but this was a bit of a broad brush question: I suspect the OP's services are far more complex. As I've worked on projects, I've increasingly hit issues with not matching services to the scope of their consumers. And dealing correctly with `IDisposable` is all too often overlooked! – MrC aka Shaun Curtis Feb 24 '23 at 11:28
  • Using `ActivatorUtilities` just gives you a `Transient` Service with no strings [IDisposable references] attached. – MrC aka Shaun Curtis Feb 24 '23 at 11:35