MediatR如何知道调用哪个处理程序?

7

我正在学习MediatR,但是我遇到了将控制器连接到处理程序的问题。

控制器:

[HttpGet]
public async Task<List<PersonModel>> Get()
{
    return await _mediator.Send(new GetPersonListQuery());
}

我的理解是Controller将使用_mediator.Send()调用Handler。然而,Send的参数不是Handler,而是Command / Query。

在Visual Studio中,我检查了对Handler的引用数量,并显示为零。那么MediatR如何知道要调用哪个特定的Handler呢?

谢谢。


@Konrad Rudolph 感谢您的编辑。 - EBDS
3个回答

10

MediatR在其机制中大量使用泛型类型。从使用它时您可能已经注意到,为了使您的请求和处理程序可用于MediatR,您需要以正确的方式使用某些接口。

让我们以您的示例来解释这一切是如何工作的:

await _mediator.Send(new GetPersonListQuery());

为了使 GetPersonListQuery 被接受,它需要实现标记接口 IRequest<TResponse>(其中 TResponse 代表任何响应类型)。如果您的请求类隐式地实现了此接口,例如通过实现 IRequest(这是 IRequest<Unit> 的快捷方式),则也可以运行。因此,GetPersonListQuery 实现了请求接口,使其成为 MediatR 的有效请求类型。为了使处理程序能够对其进行响应,该处理程序现在将需要实现 IRequestHandler<GetPersonListQuery>(同样,隐式实现也可以)。因此,您现在有了这个星座:
public class GetPersonListQuery : IRequest { }
public class PersonListHandler : IRequestHandler<GetPersonListQuery> { }

从使用 MediatR 的角度来看,这通常是你需要做的全部。MediatR 将连接它们,因此当发送请求时处理程序将运行。

它依赖于依赖注入,最常见的是 Microsoft.Extensions.DependencyInjection 容器。依赖注入容器基本上通过收集包含服务类型和实现类型的服务注册列表来工作。然后,当容器需要解析服务类型的实例时,它将查找该服务的服务注册表,并通过实例化实现类型来构造一个对象。

在我们的示例中,PersonListHandler 是实现类型,而 IRequestHandler<GetPersonListQuery> 接口是服务类型。因此,当某些东西从 DI 容器请求 IRequestHandler<GetPersonListQuery> 时,DI 容器将用 PersonListHandler 实例响应它。

所以 MediatR 实际上并没有做太多事情;事实上,它大部分的复杂性来自于抽象化依赖注入容器,使其也可以与其他容器一起工作。但是如果您想使用特定的 DI 容器来实现这个功能,那么实现看起来可能像这样(简化伪代码):

public Task<object> Send(IRequest request)
{
    var requestType = request.GetType(); // GetPersonListQuery
    var handlerType = CreateHandlerType(requestType); // IRequestHandler<GetPersonListQuery>

    var handler = GetService(handlerType); // PersonListHandler object
    return await handler.Run(request);
}

因此,魔法大部分都归结于 DI 容器的魔法,MediatR 利用容器解析服务的能力。但是,DI 容器如何知道要注册哪些处理程序呢?这很容易解释:当您执行 services.AddMediatR(...)时,实际上是告诉 MediatR 主动扫描程序集,查找所有实现 IRequestHandler 的类型,并将它们注册到依赖注入容器中。因此,只要在项目中添加新的请求处理程序,AddMediatR 在应用程序启动时就会捕获到它,使其可用于 DI 容器,并由此可以让 MediatR 中介实现使用。
由于 MediatR 依赖于 DI 容器以解析请求处理程序,这也直接回答了您提出的另一个问题:当一个请求类型有多个请求处理程序时会发生什么?
默认情况下,DI 容器只会在解析服务时提供一个实例。这意味着,当您有多个实现 IRequestHandler 的处理程序时,您只会得到其中一个。通常,DI 容器将使用最新的服务注册来解析服务。因此,如果您为同一服务类型注册了两个实现类型,则后者将覆盖前者。
由于您不能使用 AddMediatR 自己注册服务,因此无法主动控制注册顺序。由于它会遍历所有类型,因此我期望注册顺序是按字母顺序排列的,但我不会依赖于这一点。如果您想要运行特定的处理程序,则应避免在同一个程序集中拥有不同的处理程序。
请注意,有些情况下,多个服务注册确实有效:MediatR 中的整个通知概念(INotification 和 INotificationHandler)就是为了支持多个处理程序而设计的。这使用依赖注入容器中的不同功能,该功能允许您解析出所有服务注册的实例。因此,如果您有两个针对同一通知的处理程序,则 DI 容器将返回这两个处理程序,MediatR 可以同时调用它们。MediatR 中的其余处理与请求完全相同。

1
非常好的答案。我现在明白了。非常感谢您清晰详细的解释!非常感激。 - EBDS

3

在运行时,MediatR会扫描您的项目以查找实现IRequestHandler<T>的类,并记下每个类(包括您的IRequestHandler<GetPersonListQuery>)。当您使用_mediator.Send时,它会将参数的类型与应用程序启动期间发现的匹配的IRequestHandler进行匹配。


所以我不需要显式地调用处理程序。如果找到两个具有相同签名的处理程序,会发生什么情况?作为程序文件,它们彼此不知道,因此拥有相同签名的两个文件是有效的,可能位于不同的命名空间中。当程序扫描并找到这两个文件后,如何解决它?非常感谢! - EBDS
3
如果发现两个处理程序具有相同的签名,会发生什么?我猜测它会抛出一个异常。 - Orion
1
所以我不必显式地调用处理程序,它的“魔力”就在于此。 - daremachine

2
当您创建查询/命令时,您使用IRequest接口。然后在处理程序中,您调用IRequestHandler<Query/Command>。在这里它们建立关系。

谢谢您的回复。我不明白它如何知道将它们连接起来?如果我有两个处理程序包含相同的<查询/命令>,会发生什么? - EBDS
1
一个命令/查询不可能有两个处理程序。 - Soudabeh Parsa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接