如何使用.NET CORE DI 为同一接口注册多个实现

Bud*_*lum 3 c# dependency-injection asp.net-web-api asp.net-core

我有一个名为 ICompleteConsumer 的接口,它只有一种方法,并且该接口有多个实现。

前任:

public class SampleClass1: ICompleteConsumer {

 public async Task Complete(Model model) {
  // do the work
 }
}

public class SampleClass2: ICompleteConsumer {

 public async Task Complete(Model model) {
  // do the work
 }
}
Run Code Online (Sandbox Code Playgroud)

我像这样在启动中注册了这些

services.AddScoped<ICompleteConsumer, SampleClass1>(),
services.AddScoped<ICompleteConsumer, SampleClass2>(), 
Run Code Online (Sandbox Code Playgroud)

我注入这些依赖项如下

public class ConsumerHandlerService(string message) {

 private readonly ICompleteConsumer completeConsumer;

 public ConsumerHandlerService(ICompleteConsumer completeConsumer) {
  this.completeConsumer = completeConsumer
 }

 switch (MessageType) {
  case (MessageType .1) //I want SampleClass1 implementation  to here
  var model = JsonConvert.DeserializeObject < Model > (message);
  await completeConsumer.complete(model);
  break;

  case (MessageType .2) // I want SampleClass2 implementation to here
  var model = JsonConvert.DeserializeObject < Model > (message);
  await completeConsumer.complete(model);
  break;
 }
}
Run Code Online (Sandbox Code Playgroud)

如何使用 .net core DI 实现此目的?

Enr*_*one 6

你的问题其实有两个值得商榷的地方。一是与代码设计相关的点,另一点是与如何使用 .NET core DI 容器来处理所需注册相关的点。两者都很重要,但我们需要一次只对待其中一个。

如何组织代码

为了以干净且可扩展的方式解决您的问题,您需要使用称为复合设计模式的设计模式。为此,您需要将接口的定义更改为以下内容:

public interface IMessageConsumer 
{
  bool CanHandleMessage(Message message);
  Task HandleMessage(Message message);
}
Run Code Online (Sandbox Code Playgroud)

然后,您的接口实现将更改如下:

public class FooMessageConsumer: IMessageConsumer 
{
  public bool CanHandleMessage(Message message)
  {
    if (message is null) throw new ArgumentNullException(nameof(message));

    return message.Type == "foo";
  }

  public Task HandleMessage(Message message)
  {
    if (message is null) 
        throw new ArgumentNullException(nameof(message));

    if (!this.CanHandleMessage(message)) 
        throw new InvalidOperationException($"{nameof(FooMessageConsumer)} can only handle foo messages.");

    await Task.Delay(100).ConfigureAwait(false);

    Console.Writeline($"Message {message.Id} handled by {nameof(FooMessageConsumer)}");
  }
}

public class BarMessageConsumer: IMessageConsumer 
{
  public bool CanHandleMessage(Message message)
  {
    if (message is null) throw new ArgumentNullException(nameof(message));

    return message.Type == "bar";
  }

  public Task HandleMessage(Message message)
  {
    if (message is null) 
        throw new ArgumentNullException(nameof(message));

    if (!this.CanHandleMessage(message)) 
        throw new InvalidOperationException($"{nameof(BarMessageConsumer)} can only handle bar messages.");

    await Task.Delay(100).ConfigureAwait(false);

    Console.Writeline($"Message {message.Id} handled by {nameof(BarMessageConsumer)}");
  }
}
Run Code Online (Sandbox Code Playgroud)

此时您需要引入一个特殊的消息消费者,它将用于将消息分派给正确的消费者。IMessageConsumer这称为复合消息使用者,这是您将在 DI 容器中注册的实现,并将被注入到需要消息使用者来完成其业务的所有类中。

public class CompositeMessageConsumer : IMessageConsumer 
{
  private readonly IMessageConsumer[] _consumers;

  public CompositeMessageConsumer(IEnumerable<IMessageConsumer> consumers)
  {
    if (consumers is null)
        throw new ArgumentNullException(nameof(consumers));

    this._consumers = consumers.ToArray();
  }

  public bool CanHandleMessage(Message message)
  {
    if (message is null) throw new ArgumentNullException(nameof(message));

    return this._consumers.Any(c => c.CanHandleMessage(message));
  }

  public async Task HandleMessage(Message message)
  {
    if (message is null) 
        throw new ArgumentNullException(nameof(message));

    if (!this.CanHandleMessage(message)) 
        throw new InvalidOperationException("None of the available consumers is able to handle the provided message.");

    var consumer = this._consumers.First(c => c.CanHandleMessage(message));
    await consumer.HandleMessage(message).ConfigureAwait(false);
  }
}
Run Code Online (Sandbox Code Playgroud)

这是使用该接口的类的示例IMessageConsumer。在运行时,DI 容器将注入CompositeMessageConsumer.

// this is an example of a class depending on the IMessageConsumer service
public class MessageProcessor 
{
  // at runtime this will be an instance of CompositeMessageConsumer
  private readonly IMessageConsumer _consumer;

  // the DI container will inject an instance of CompositeMessageConsumer here
  public MessageProcessor(IMessageConsumer consumer) 
  {
    if (consumer is null) throw new ArgumentNullException(nameof(consumer));

    this._consumer = consumer;
  }

  public async Task ProcessIncomingMessage(Message message)
  {
    if (message is null) throw new ArgumentNullException(nameof(message));

    // do all the pre processing here...

    // handle the message
    await this._consumer.HandleMessage(message).ConfigureAwait(false);

    // do all the post processing here...
  }
}
Run Code Online (Sandbox Code Playgroud)

如何在.NET core DI容器上注册服务

确定注册的适当生命周期是一个超出本讨论范围的问题。

在上面的示例代码中,我定义了无状态消费者类,复合消费者仅迭代可用消费者的数组。该数组在迭代过程中永远不会被修改。这意味着所有涉及的类都是线程安全的,因此我们可以使用单例生命周期注册所有它们。

也就是说,您可以执行的最简单的注册如下:

// register the consumers as classes
services.AddSingleton<FooMessageConsumer>();
service.AddSingleton<BarMessageConsumer>();

// register the composite message consumer as an interface, so that when you require IMessageConsumer you get CompositeMessageConsumer
services.AddSingleton<IMessageConsumer>(container => 
{
    var fooConsumer = container.GetRequiredService<FooMessageConsumer>();
    var barConsumer = container.GetRequiredService<BarMessageConsumer>();

    return new CompositeMessageConsumer(new IMessageConsumer[] 
    {
        fooConsumer,
        barConsumer
    });
});
Run Code Online (Sandbox Code Playgroud)

这本书是一本了解这些主题的好书。如果您是一名 .NET 开发人员,这绝对是必读的一本书。