目录
如何使用 MediatR
MediatR的基本用法非常简单。首先,安装MediatR NuGet包。在您的应用程序中,您将对要完成的工作有不同的描述(例如,创建一个ToDo项、更改用户名等)。这些描述在MediatR中称为请求。它们是实现IRequest<T>接口的简单类。这是一个没有任何成员的标记接口。
class CreateToDoItem : IRequest<int>
{
public string ToDoItemText { get; set; }
}
这些类可能不包含任何逻辑,它们只是您操作的数据容器。
但是IRequest<T>接口中的T类型参数是什么?你看,你的操作可能会返回一些结果。例如,如果您正在创建一个新的ToDo项目,您可能需要获取该项目的ID。这正是T的用途。在我们的例子中,我们想要获取新ToDo项的整数ID。
现在,我们需要一些代码来执行这个操作。MediatR调用此代码请求处理程序。请求处理程序必须实现IRequestHandler<TRequest, TResponse>接口,其中TRequest必须是IRequest<TResponse>:
class CreateToDoItemHandler : IRequestHandler<CreateToDoItem, int>
{
public Task<int> Handle(CreateToDoItem request, CancellationToken cancellationToken)
{
...
}
}
如您所见,此接口需要实现一个Handle异步执行请求操作并返回所需结果的方法。
剩下要做的就是连接请求和相应的处理程序。MediatR使用依赖容器执行此操作。如果您开发ASP.NET Core应用程序,那么您可以使用MediatR的MediatR.Extensions.Microsoft.DependencyInjection包。但是MediatR支持许多不同的容器。
services.AddMediatR(typeof(Startup));
services是一个IServiceCollection接口实例,通常可以在Startup类的ConfigureServices方法中访问。此命令将扫描Startup类所在的程序集并查找所有请求处理程序。
现在您可以执行您的请求了。您只需要获取对IMediator实例的引用。它使用相同的AddMediatR方法在您的容器中注册。
var toDoItemId = await mediator.Send(createToDoItemRequest);
就这样。MediatR将找到适当的请求处理程序,执行它,并将结果返回给您。
现在,我们来解决主要问题。
为什么我们需要MediatR?
假设我们有一个支持ToDo项操作的ASP.NET Core控制器。我们将比较如何使用MediatR和不使用它来实现ToDo项创建。这是没有MediatR的代码:
[ApiController]
public class ToDoController : ControllerBase
{
private readonly IToDoService _service;
public ToDoController(IToDoService service)
{
_service = service;
}
[HttpPost]
public async Task<IActionResult> CreateToDoItem
([FromBody] CreateToDoItem createToDoItemRequest)
{
var toDoItemId = await _service.CreateToDoItem(createToDoItemRequest);
return Ok(toDoItemId);
}
}
现在使用MediatR实现相同的实现:
[ApiController]
public class ToDoController : ControllerBase
{
private readonly IMediator _mediator;
public ToDoController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateToDoItem([FromBody]
CreateToDoItem createToDoItemRequest)
{
var toDoItemId = await _mediator.Send(createToDoItemRequest);
return Ok(toDoItemId);
}
}
您在这里看到MediatR的任何重大优势吗?我没有。事实上,我认为MediatR的实现可读性差一些。它使用通用Send方法而不是有意义的CreateToDoItem方法。
那么我为什么要使用MediatR?
参考
首先,MediatR将请求处理程序与请求分开。在我们的控制器代码中,我们不引用CreateToDoItemHandler类。这意味着我们可以将这个类移动到同一个程序集中的任何地方,我们不需要修改控制器的代码。
但就个人而言,我不认为这是一个很大的优势。是的,对您的项目进行一些更改会更容易。但与此同时,我们也会在这里遇到一些困难。从我们控制器的代码中,我们实际上看不到谁在处理我们的请求。要找到CreateToDoItem的实例的处理程序,我们需要知道MediatR是什么以及它是如何工作的。这里没有什么特别复杂的。毕竟IToDoService也不是一个处理程序实现,我们将不得不寻找实现这个接口的类。但新开发人员仍需要更多时间来弄清楚发生了什么。
单一职责
下一个区别更重要。你看,请求处理程序是一个类。而这整个类负责执行单个操作。对于服务(例如IToDoService),一种方法负责执行一项操作。这意味着服务可以包含许多不同的方法,可能与不同的操作相关。这使得很难理解服务代码。另一方面,整个请求处理程序类负责单个操作。这使得这个类更小更容易理解。
一切看起来都不错,但实际情况稍微复杂一些。通常,您必须支持很多相关的操作(例如,创建ToDo项、更新ToDo项、更改ToDo项的状态,...)。所有这些操作可能需要相同的代码。在服务的情况下,我们可以使用私有方法来做普通的工作。但是请求处理程序是单独的类。当然,我们可以使用继承并将我们需要的所有内容提取到基类中。但这给我们带来了同样的情况,如果不是更糟的话。在服务的情况下,我们在一个类中有很多方法。现在我们有许多分布在多个类中的方法。我不确定哪个更好。
换句话说,如果你想打腿,你还有很多选择。
装饰器
但是MediatR还有一个更重要的优势。你看,你所有的请求处理程序都实现了相同的接口IRequestHandler。这意味着您可以编写适用于所有这些的装饰器。在ASP.NET Core中,您可以使用Scrutor NuGet包来支持装饰器。例如,您可以编写日志装饰器:
class LoggingDecorator<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IRequestHandler<TRequest, TResponse> _handler;
private readonly Logger _logger;
public LoggingDecorator(IRequestHandler<TRequest, TResponse> handler,
Logger logger)
{
_handler = handler;
_logger = logger;
}
public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
{
_logger.Log("Log something here.");
return _handler.Handle(request, cancellationToken);
}
}
现在注册它:
services.AddMediatR(typeof(Startup));
services.Decorate(typeof(IRequestHandler<,>), typeof(LoggingDecorator<,>));
就这样。现在您将日志记录应用到所有请求处理程序。您不需要为每个服务创建单独的装饰器。您所需要的只是装饰一个接口。
但是为什么要打扰Scrutor呢?MediatR提供与管道行为相同的功能。编写一个类实现IPipelineBehavior:
class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly Logger _logger;
public LoggingBehavior(Logger logger)
{
_logger = logger;
}
public async Task<TResponse> Handle
(TRequest request, CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
try
{
_logger.Log($"Before execution for {typeof(TRequest).Name}");
return await next();
}
finally
{
_logger.Log($"After execution for {typeof(TRequest).Name}");
}
}
}
注册它:
services.AddMediatR(typeof(Startup));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
一切都以同样的方式工作。你不再需要装饰器了。所有注册的管道行为都将按照注册顺序与每个请求处理程序一起执行。
带有行为的方法甚至比装饰器更好。考虑以下示例。您可能希望在事务中执行一些请求。为了标记此类请求,您使用ITransactional标记接口:
interface ITransactional { }
class CreateToDoItem : IRequest<int>, ITransactional
...
如何仅将您的行为应用于标有ITransactional接口的请求?您可以使用通用类约束:
class TransactionalBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : ITransactional
...
但是你不能对Scrutor装饰器做同样的事情。如果你像这样实现装饰器:
class TransactionalDecorator<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse>, ITransactional
...
如果您有任何未实现的请求,您将无法使用它ITransactional。
在实现管道行为时请记住,它们在每次调用Send方法时执行。如果您从处理程序内部发送请求,这可能很重要:
class CommandHandler : IRequestHandler<Command, string>
{
private readonly IMediator _mediator;
public CommandHandler(IMediator mediator)
{
_mediator = mediator;
}
public async Task<string> Handle(Command request, CancellationToken cancellationToken)
{
...
var result = await _mediator.Send(new AnotherCommand(), cancellationToken);
...
}
}
如果你同时用ITransactional接口标记了Command和AnotherCommand,对应的TransactionalBehavior将被执行两次。因此,请确保您没有创建两个单独的事务。
其他功能
MediatR还为您提供其他功能。它支持通知机制。如果您在架构中使用域事件,它可能会非常有用。您的所有事件类都必须实现INotification标记接口。您可以使用INotificationHandler接口为这种事件类型创建任意数量的处理程序。请求和通知的区别如下。请求将仅传递给一个处理程序。通知将传递给此类通知的所有已注册处理程序。此外,对于请求,您可以获得其处理的结果。通知不允许获得任何结果。使用Publish方法发送通知。
MediatR还提供异常处理机制。它相当复杂,您可以在此处阅读。
结论
总之,我不得不说MediatR是一个有趣的NuGet包。使用单个接口和行为机制来表达所有操作的能力使其在我的项目中使用起来很有吸引力。我不能说它是灵丹妙药,但它有一定的优势。
https://www.codeproject.com/Articles/5317666/Why-Do-We-Need-MediatR