英文:
Declare Dependency Injection when the constructor takes a paramter
问题
在.NET 6.0 Core控制台应用程序中学习DI
:
我已经编写了这个:
static void Main(string[] args)
{
var serviceProvider = new ServiceCollection()
.AddSingleton<InterfaceOne, ClassOne>()
.AddSingleton<InterfaceTwo, ClassTwo>()
.BuildServiceProvider();
var one = serviceProvider.GetRequiredService<InterfaceOne>(); // 可行。它的构造函数没有参数。
var two = serviceProvider.GetRequiredService<InterfaceTwo>(); // 不可行。它的构造函数有一个参数。
one.JustSayHi();
//two.SayWhateverMessageIsPassedToYou();
}
ClassOne
实现了 InterfaceOne
,它非常简单,在 constructor
中没有特殊的内容,所以我能够运行它。但是后来我创建了 ClassTwo
和 InterfaceTwo
,但现在我不知道如何将参数传递给该类。这是我的 ClassTwo
:
public interface InterfaceTwo
{
void SayWhateverMessageIsPassedToYou();
}
public class ClassTwo : InterfaceTwo
{
private readonly string _sayThisThing;
public ClassTwo(string sayThisThing)
{
_sayThisThing = sayThisThing;
}
public void SayWhateverMessageIsPassedToYou()
{
Console.WriteLine(_sayThisThing);
}
}
英文:
Learning DI
in a .NET 6.0 Core
Console Application:
I have written this:
static void Main(string[] args)
{
var serviceProvider = new ServiceCollection()
.AddSingleton<InterfaceOne, ClassOne>()
.AddSingleton<InterfaceTwo, ClassTwo>()
.BuildServiceProvider();
var one = serviceProvider.GetRequiredService<InterfaceOne>(); // works. Its CTOR has no params.
var two = serviceProvider.GetRequiredService<InterfaceTwo>(); // does not work. Its CTOR has a param.
one.JustSayHi();
//two.SayWhateverMessageIsPassedToYou();
}
ClassOne
implements InterfaceOne
and it is super simple, nothing special in constructor
so I was able to run that one. But then I created ClassTwo
and InterfaceTwo
but I don't know now how to pass my parameter to that class. Here is my ClassTwo :
public interface InterfaceTwo
{
void SayWhateverMessageIsPassedToYou();
}
public class ClassTwo: InterfaceTwo
{
private readonly string _sayThisThing;
public ClassTwo(string sayThisThing)
{
_sayThisThing = sayThisThing;
}
public void SayWhateverMessageIsPassedToYou()
{
Console.WriteLine(_sayThisThing);
}
}
答案1
得分: 2
Brian的回答在详细说明了一些选项的同时,跳过了我认为最好的选项,如果可能的话:将您的代码重构以将服务与值分开。
例如,如果字符串值可能会随着应用程序的上下文而变化(例如,它作为请求参数提供),那么接口应该被更改为将字符串值作为方法参数:
public interface InterfaceTwo
{
void SayWhateverMessageIsPassedToYou(string sayThisThing);
}
...然后,您的实现可以使用其构造函数来注入它所使用的服务。例如,注入控制台将使该类更容易在隔离的单元测试中进行测试:
public class ClassTwo : InterfaceTwo
{
TextWriter _output;
public ClassTwo(TextWriter output)
{
_output = output;
}
public void SayWhateverMessageIsPassedToYou(string sayThisThing)
{
_output.WriteLine(sayThisThing);
}
}
var serviceProvider = new ServiceCollection()
.AddSingleton<InterfaceTwo, ClassTwo>()
.AddSingleton<TextWriter>(Console.Out)
.BuildServiceProvider();
如果字符串不随上下文而变化,因此更改方法签名没有意义,考虑注入一个独立的服务,该服务充当该字符串的提供者,供您的类使用。
public class ClassTwo : InterfaceTwo
{
TextWriter _output;
IMessageProvider _messageProvider;
public ClassTwo(TextWriter output, IMessageProvider messageProvider)
{
_output = output;
_messageProvider = messageProvider;
}
public void SayWhateverMessageIsPassedToYou()
{
_output.WriteLine(_messageProvider.GetMessage());
}
}
public interface IMessageProvider
{
string GetMessage();
}
public record FixedMessageProvider(string Message) : IMessageProvider
{
public string GetMessage() => Message;
}
var serviceProvider = new ServiceCollection()
.AddSingleton<InterfaceTwo, ClassTwo>()
.AddSingleton<TextWriter>(Console.Out)
.AddSingleton<IMessageProvider>(new FixedMessageProvider("Hi"))
.BuildServiceProvider();
这是用于在ASP.NET Core中获取配置选项的模式示例:https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-7.0
英文:
While Brian's answer does a great job at spelling out some options, it skips the option which I would consider best of all, if it's possible: Refactor your code to separate Services from Values.
For example, if the string value might change with the context of your application (e.g. it's provided as a request parameter), then the interface should be changed to take the string value as a method parameter:
public interface InterfaceTwo
{
void SayWhateverMessageIsPassedToYou(string sayThisThing);
}
... then your implementation could use its constructor to inject the services that it uses. For example, injecting the console would make the class easier to unit test in isolation:
public class ClassTwo : InterfaceTwo
{
TextWriter _output;
public ClassTwo(TextWriter output)
{
_output = output;
}
public void SayWhateverMessageIsPassedToYou(string sayThisThing)
{
_output.WriteLine(sayThisThing);
}
}
var serviceProvider = new ServiceCollection()
.AddSingleton<InterfaceTwo, ClassTwo>()
.AddSingleton<TextWriter>(Console.Out)
.BuildServiceProvider();
If the string doesn't change with context, so it doesn't make sense to change the method signature, consider injecting a separate service which acts as the provider of that string for your class to use.
public class ClassTwo : InterfaceTwo
{
TextWriter _output;
IMessageProvider _messageProvider;
public ClassTwo(TextWriter output, IMessageProvider messageProvider)
{
_output = output;
_messageProvider = messageProvider;
}
public void SayWhateverMessageIsPassedToYou()
{
_output.WriteLine(_messageProvider.GetMessage());
}
}
public interface IMessageProvider
{
string GetMessage();
}
public record FixedMessageProvider(string Message) : IMessageProvider
{
public string GetMessage() => Message;
}
var serviceProvider = new ServiceCollection()
.AddSingleton<InterfaceTwo, ClassTwo>()
.AddSingleton<TextWriter>(Console.Out)
.AddSingleton<IMessageProvider>(new FixedMessageProvider("Hi"))
.BuildServiceProvider();
This is the pattern used, for example, to get configuration options in ASP.NET Core: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-7.0
答案2
得分: 1
<h1>方法 1</h1>
为了直接回答您的问题,虽然我不建议这种方法:
var serviceProvider = new ServiceCollection()
.AddSingleton<InterfaceOne, ClassOne>()
.AddSingleton<InterfaceTwo, ClassTwo>(sp => new ClassTwo("Hello, World!"))
.BuildServiceProvider();
以下是完整上下文中的答案,附带了一个示例控制台应用程序...
https://github.com/CodeFontana/FoobarConsoleApp
FoobarConsoleUI.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
</ItemGroup>
</Project>
Program.cs
using FoobarConsoleUI;
using FoobarConsoleUI.Interfaces;
using FoobarConsoleUI.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Services.AddTransient<IFooService, FooService>();
builder.Services.AddTransient<IBarService, BarService>(sp =>
{
ILogger<BarService> barLogger = sp.GetRequiredService<ILogger<BarService>>();
return new BarService(barLogger, "Foobar");
});
builder.Services.AddHostedService<App>();
IHost app = builder.Build();
app.Run();
App.cs
using FoobarConsoleUI.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace FoobarConsoleUI;
public class App : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IConfiguration _config;
private readonly ILogger<App> _logger;
private readonly IFooService _fooService;
private readonly IBarService _barService;
public App(IHostApplicationLifetime hostApplicationLifetime,
IConfiguration configuration,
ILogger<App> logger,
IFooService fooService,
IBarService barService)
{
_hostApplicationLifetime = hostApplicationLifetime;
_config = configuration;
_logger = logger;
_fooService = fooService;
_barService = barService;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_hostApplicationLifetime.ApplicationStarted.Register(async () =>
{
try
{
await Task.Yield(); // https://github.com/dotnet/runtime/issues/36063
await Task.Delay(1000); // Additional delay for Microsoft.Hosting.Lifetime messages
Execute();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception!");
}
finally
{
_hostApplicationLifetime.StopApplication();
}
});
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public void Execute()
{
_fooService.Foo();
_barService.Bar();
}
}
IFooService:
namespace FoobarConsoleUI.Interfaces;
public interface IFooService
{
void Foo();
}
FooService
using FoobarConsoleUI.Interfaces;
using Microsoft.Extensions.Logging;
namespace FoobarConsoleUI.Services;
public class FooService : IFooService
{
private readonly ILogger<FooService> _logger;
public FooService(ILogger<FooService> logger)
{
_logger = logger;
}
public void Foo()
{
_logger.LogInformation("Foo");
}
}
IBarService:
namespace FoobarConsoleUI.Interfaces;
public interface IBarService
{
void Bar();
}
BarService:
using FoobarConsoleUI.Interfaces;
using Microsoft.Extensions.Logging;
namespace FoobarConsoleUI.Services;
public class BarService : IBarService
{
private readonly ILogger<BarService> _logger;
private readonly string _inputText;
public BarService(ILogger<BarService> logger,
string inputText)
{
_logger = logger;
_inputText = inputText;
}
public void Bar()
{
_logger.LogInformation($"Bar: {_inputText}");
}
}
<h1>方法 2</h1>
如评论中所讨论的,简单地不要求构造函数参数,而是使用属性。在这种方法中,在执行方法之前在运行时设置属性。这种方法的缺点是逻辑错误,即在设置属性之前调用SayWhateverMessageIsPassedToYou()方法。所以要小心。
我认为这种方法很简单,所以我不会提供代码示例。
<h1>方法 3</h1>
在某些情况下,您将不得不在运行时传递构造函数参数,这是您最终会遇到的情况。因此,最好的方法是实现一个工厂类,该类将被注入到您的DI中,并且在运行时可以使用正确的参数实例化您所需的类。
以下是从方法1中重新编写的相同程序,以使用工厂类...
Program.cs
using FoobarFactoryConsoleUI;
using FoobarFactoryConsoleUI.Factories;
using FoobarFactoryConsoleUI.Interfaces;
using FoobarFactoryConsoleUI.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Services.AddTransient<IFooService, FooService>();
builder.Services.AddTransient<BarServiceFactory>();
builder.Services.AddHostedService<App>();
IHost app = builder.Build();
app.Run();
App.cs
using FoobarFactoryConsoleUI.Factories;
using FoobarFactoryConsoleUI.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace FoobarFactoryConsoleUI;
public class App
<details>
<summary>英文:</summary>
<h1>Method 1</h1>
To most directly answer your question, though I would discourage this approach:
var serviceProvider = new ServiceCollection()
.AddSingleton<InterfaceOne, ClassOne>()
.AddSingleton<InterfaceTwo, ClassTwo>(sp => new ClassTwo("Hello, World!"))
.BuildServiceProvider();
Here's the answer in full context with a sample console app...
https://github.com/CodeFontana/FoobarConsoleApp
FoobarConsoleUI.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
</ItemGroup>
</Project>
Program.cs
using FoobarConsoleUI;
using FoobarConsoleUI.Interfaces;
using FoobarConsoleUI.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Services.AddTransient<IFooService, FooService>();
builder.Services.AddTransient<IBarService, BarService>(sp =>
{
ILogger<BarService> barLogger = sp.GetRequiredService<ILogger<BarService>>();
return new BarService(barLogger, "Foobar");
});
builder.Services.AddHostedService<App>();
IHost app = builder.Build();
app.Run();
App.cs
using FoobarConsoleUI.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace FoobarConsoleUI;
public class App : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IConfiguration _config;
private readonly ILogger<App> _logger;
private readonly IFooService _fooService;
private readonly IBarService _barService;
public App(IHostApplicationLifetime hostApplicationLifetime,
IConfiguration configuration,
ILogger<App> logger,
IFooService fooService,
IBarService barService)
{
_hostApplicationLifetime = hostApplicationLifetime;
_config = configuration;
_logger = logger;
_fooService = fooService;
_barService = barService;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_hostApplicationLifetime.ApplicationStarted.Register(async () =>
{
try
{
await Task.Yield(); // https://github.com/dotnet/runtime/issues/36063
await Task.Delay(1000); // Additional delay for Microsoft.Hosting.Lifetime messages
Execute();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception!");
}
finally
{
_hostApplicationLifetime.StopApplication();
}
});
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public void Execute()
{
_fooService.Foo();
_barService.Bar();
}
}
IFooService:
namespace FoobarConsoleUI.Interfaces;
public interface IFooService
{
void Foo();
}
FooService
using FoobarConsoleUI.Interfaces;
using Microsoft.Extensions.Logging;
namespace FoobarConsoleUI.Services;
public class FooService : IFooService
{
private readonly ILogger<FooService> _logger;
public FooService(ILogger<FooService> logger)
{
_logger = logger;
}
public void Foo()
{
_logger.LogInformation("Foo");
}
}
IBarService:
namespace FoobarConsoleUI.Interfaces;
public interface IBarService
{
void Bar();
}
BarService:
using FoobarConsoleUI.Interfaces;
using Microsoft.Extensions.Logging;
namespace FoobarConsoleUI.Services;
public class BarService : IBarService
{
private readonly ILogger<BarService> _logger;
private readonly string _inputText;
public BarService(ILogger<BarService> logger,
string inputText)
{
_logger = logger;
_inputText = inputText;
}
public void Bar()
{
_logger.LogInformation($"Bar: {_inputText}");
}
}
Output:
[![enter image description here][1]][1]
[1]: https://i.stack.imgur.com/bmbmG.png
<h1>Method 2</h1>
As discussed in the comments, simply don't require the constructor parameter, and use a property instead. In this approach, the property is passed at runtime before executing your method.
The downside to this approach is a logic error, where you call your SayWhateverMessageIsPassedToYou() before your property is set. So be careful.
I think this approach is straightforward, so I won't provide a code sample.
<h1>Method 3</h1>
In the scenario, which you will run into eventually, sometimes you just have to pass a constructor parameter at runtime. Thus the best approach is to implement a factory class, which would get injected into your DI, and at runtime can instantiate your required class with the proper parameters.
Here's the same program from Method 1, rewritten to use a factory class...
Program.cs
using FoobarFactoryConsoleUI;
using FoobarFactoryConsoleUI.Factories;
using FoobarFactoryConsoleUI.Interfaces;
using FoobarFactoryConsoleUI.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Services.AddTransient<IFooService, FooService>();
builder.Services.AddTransient<BarServiceFactory>();
builder.Services.AddHostedService<App>();
IHost app = builder.Build();
app.Run();
App.cs
using FoobarFactoryConsoleUI.Factories;
using FoobarFactoryConsoleUI.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace FoobarFactoryConsoleUI;
public class App : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IConfiguration _config;
private readonly ILogger<App> _logger;
private readonly IFooService _fooService;
private readonly BarServiceFactory _barFactory;
public App(IHostApplicationLifetime hostApplicationLifetime,
IConfiguration configuration,
ILogger<App> logger,
IFooService fooService,
BarServiceFactory barFactory)
{
_hostApplicationLifetime = hostApplicationLifetime;
_config = configuration;
_logger = logger;
_fooService = fooService;
_barFactory = barFactory;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_hostApplicationLifetime.ApplicationStarted.Register(async () =>
{
try
{
await Task.Yield(); // https://github.com/dotnet/runtime/issues/36063
await Task.Delay(1000); // Additional delay for Microsoft.Hosting.Lifetime messages
Execute();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception!");
}
finally
{
_hostApplicationLifetime.StopApplication();
}
});
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public void Execute()
{
_fooService.Foo();
IBarService barService = _barFactory.CreateInstance("Hello, World!");
barService.Bar();
}
}
BarServiceFactory.cs
using FoobarFactoryConsoleUI.Interfaces;
using FoobarFactoryConsoleUI.Services;
using Microsoft.Extensions.Logging;
namespace FoobarFactoryConsoleUI.Factories;
public class BarServiceFactory
{
private readonly ILoggerFactory _loggerFactory;
public BarServiceFactory(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public IBarService CreateInstance(string inputText)
{
ILogger<BarService> _barLogger = _loggerFactory.CreateLogger<BarService>();
return new BarService(_barLogger, inputText);
}
}
The rest of the code is exactly the same as Method 1 above.
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论