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
builder.Services.AddSingleton<IDataService>(new DataService());
Drawbacks
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
Drawbacks
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!
The simple approach is also "best practice"
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.
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:
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With