Declare Dependency Injection when the constructor takes a parameter.

huangapple go评论71阅读模式
英文:

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 中没有特殊的内容,所以我能够运行它。但是后来我创建了 ClassTwoInterfaceTwo,但现在我不知道如何将参数传递给该类。这是我的 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&lt;InterfaceOne, ClassOne&gt;()
            .AddSingleton&lt;InterfaceTwo, ClassTwo&gt;()
            .BuildServiceProvider();
        
        var one = serviceProvider.GetRequiredService&lt;InterfaceOne&gt;(); // works. Its CTOR has no params.
        var two = serviceProvider.GetRequiredService&lt;InterfaceTwo&gt;(); // 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&lt;InterfaceTwo, ClassTwo&gt;()
    .AddSingleton&lt;TextWriter&gt;(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() =&gt; Message;
}
    var serviceProvider = new ServiceCollection()
        .AddSingleton&lt;InterfaceTwo, ClassTwo&gt;()
        .AddSingleton&lt;TextWriter&gt;(Console.Out)
        .AddSingleton&lt;IMessageProvider&gt;(new FixedMessageProvider(&quot;Hi&quot;))
        .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&lt;InterfaceOne, ClassOne&gt;()
    .AddSingleton&lt;InterfaceTwo, ClassTwo&gt;(sp =&gt; new ClassTwo(&quot;Hello, World!&quot;))
    .BuildServiceProvider();

以下是完整上下文中的答案,附带了一个示例控制台应用程序...
https://github.com/CodeFontana/FoobarConsoleApp

FoobarConsoleUI.csproj

&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;

  &lt;PropertyGroup&gt;
    &lt;OutputType&gt;Exe&lt;/OutputType&gt;
    &lt;TargetFramework&gt;net7.0&lt;/TargetFramework&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
  &lt;/PropertyGroup&gt;

  &lt;ItemGroup&gt;
    &lt;PackageReference Include=&quot;Microsoft.Extensions.Hosting&quot; Version=&quot;7.0.1&quot; /&gt;
    &lt;PackageReference Include=&quot;Microsoft.Extensions.Hosting.Abstractions&quot; Version=&quot;7.0.0&quot; /&gt;
  &lt;/ItemGroup&gt;

&lt;/Project&gt;

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&lt;IFooService, FooService&gt;();
builder.Services.AddTransient&lt;IBarService, BarService&gt;(sp =&gt;
{ 
    ILogger&lt;BarService&gt; barLogger = sp.GetRequiredService&lt;ILogger&lt;BarService&gt;&gt;();
    return new BarService(barLogger, &quot;Foobar&quot;);
});
builder.Services.AddHostedService&lt;App&gt;();

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&lt;App&gt; _logger;
    private readonly IFooService _fooService;
    private readonly IBarService _barService;

    public App(IHostApplicationLifetime hostApplicationLifetime,
               IConfiguration configuration,
               ILogger&lt;App&gt; logger,
               IFooService fooService,
               IBarService barService)
    {
        _hostApplicationLifetime = hostApplicationLifetime;
        _config = configuration;
        _logger = logger;
        _fooService = fooService;
        _barService = barService;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _hostApplicationLifetime.ApplicationStarted.Register(async () =&gt;
        {
            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, &quot;Unhandled exception!&quot;);
            }
            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&lt;FooService&gt; _logger;

    public FooService(ILogger&lt;FooService&gt; logger)
    {
        _logger = logger;
    }

    public void Foo()
    {
        _logger.LogInformation(&quot;Foo&quot;);
    }
}

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&lt;BarService&gt; _logger;
    private readonly string _inputText;

    public BarService(ILogger&lt;BarService&gt; logger, 
                      string inputText)
    {
        _logger = logger;
        _inputText = inputText;
    }

    public void Bar()
    {
        _logger.LogInformation($&quot;Bar: {_inputText}&quot;);
    }
}

输出:
Declare Dependency Injection when the constructor takes a parameter.

<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&lt;IFooService, FooService&gt;();
builder.Services.AddTransient&lt;BarServiceFactory&gt;();
builder.Services.AddHostedService&lt;App&gt;();

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>

&lt;h1&gt;Method 1&lt;/h1&gt;
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&#39;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&lt;App&gt; logger,
           IFooService fooService,
           IBarService barService)
{
    _hostApplicationLifetime = hostApplicationLifetime;
    _config = configuration;
    _logger = logger;
    _fooService = fooService;
    _barService = barService;
}

public Task StartAsync(CancellationToken cancellationToken)
{
    _hostApplicationLifetime.ApplicationStarted.Register(async () =&gt;
    {
        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, &quot;Unhandled exception!&quot;);
        }
        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&lt;FooService&gt; logger)
{
    _logger = logger;
}

public void Foo()
{
    _logger.LogInformation(&quot;Foo&quot;);
}

}


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&lt;BarService&gt; logger, 
                  string inputText)
{
    _logger = logger;
    _inputText = inputText;
}

public void Bar()
{
    _logger.LogInformation($&quot;Bar: {_inputText}&quot;);
}

}


Output:
[![enter image description here][1]][1]

  [1]: https://i.stack.imgur.com/bmbmG.png

&lt;h1&gt;Method 2&lt;/h1&gt;
As discussed in the comments, simply don&#39;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&#39;t provide a code sample.

&lt;h1&gt;Method 3&lt;/h1&gt;
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&#39;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&lt;App&gt; logger,
           IFooService fooService,
           BarServiceFactory barFactory)
{
    _hostApplicationLifetime = hostApplicationLifetime;
    _config = configuration;
    _logger = logger;
    _fooService = fooService;
    _barFactory = barFactory;
}

public Task StartAsync(CancellationToken cancellationToken)
{
    _hostApplicationLifetime.ApplicationStarted.Register(async () =&gt;
    {
        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, &quot;Unhandled exception!&quot;);
        }
        finally
        {
            _hostApplicationLifetime.StopApplication();
        }
    });

    return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)
{
    return Task.CompletedTask;
}

public void Execute()
{
    _fooService.Foo();
    IBarService barService = _barFactory.CreateInstance(&quot;Hello, World!&quot;);
    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&lt;BarService&gt; _barLogger = _loggerFactory.CreateLogger&lt;BarService&gt;();
    return new BarService(_barLogger, inputText);
}

}


The rest of the code is exactly the same as Method 1 above.

</details>



huangapple
  • 本文由 发表于 2023年5月18日 02:21:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/76275131.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定