英文:
Best practice when a DI service depends on another DI service
问题
以下是要翻译的内容:
寻求关于 Blazor 最佳实践的建议/信息,当一个注入的服务依赖于另一个服务时。
我将使用 Microsoft 提供的标准 Blazor 服务器模板作为示例。我使用以下命令创建我的测试项目:`dotnet new blazorserver`
假设我的 `WeatherForecastService` 类依赖于外部数据服务 `IDataService` 来获取数据。我的接口定义如下:
```csharp
public interface IDataService
{
public string GetData();
}
以及我将用作服务的具体类定义如下:
public class DataService : IDataService
{
public string GetData()
{
// 任何实现都会与外部服务进行通信
return "一些数据在这里";
}
}
为了在 WeatherForecastService
中使用 IDataService
,我考虑了两种使用该服务的方式。
选项 1 - 将依赖项作为方法定义的一部分进行注入
我可以将依赖项注入到需要它的任何位置。例如,如果我在 WeatherForecastService
中添加一个 GetDataFromDataService
方法,它可能如下所示:
public string GetDataFromDataService(IDataService service)
{
return service.GetData();
}
优点
- 通过 Program.fs 很容易注册服务,例如:
builder.Services.AddSingleton<IDataService>(new DataService());
- 该服务对可能需要它的其他服务可用。
缺点
- 需要将需要此服务的每个方法作为参数传递(可能会变得混乱)。
- 可能需要为需要注入的
WeatherForecastService
的每个组件注入第二个服务。
选项 2 - 将依赖项作为类构造函数的一部分进行注入
作为替代方案,可以在 WeatherForecastService
构造函数的一部分中注入服务,例如:
private IDataService service { get; }
public WeatherForecastService(IDataService service)
{
this.service = service;
}
public string GetDataFromDataService()
{
return service.GetData();
}
优点
- 服务只需传入一次,可以多次重用。
缺点
- 服务将不可用于其他服务。
- 取决于构造函数的复杂程度,您可能会在
Program.fs
中执行以下操作,这会让人觉得不对劲。
var dataService = new DataService();
builder.Services.AddSingleton(new WeatherForecastService(dataService));
结论
我列出了上述选项,因为这些是我目前考虑到的选项 - 我是否漏掉了任何选项?另外,是否有关于这个问题的最佳实践,还是这取决于具体情况?
非常感谢您的任何建议!
<details>
<summary>英文:</summary>
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!
</details>
# 答案1
**得分**: 3
以下是翻译好的部分:
- 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:
``` var dataService = new DataService();
builder.Services.AddSingleton(new
WeatherForecastService(dataService)); ```
This should become
builder.Services.AddTransient
builder.Services.AddTransient<IDataService, DataService>();
part of the DI principle is that you don't `new` services.
<details>
<summary>英文:</summary>
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:
> ``` var dataService = new DataService();
> 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.
</details>
# 答案2
**得分**: 0
I agree with @HH on "good practice".
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 service 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 service 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`.
```csharp
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.
英文:
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:
- You don't want form sub-components to also use the service.
- 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论