MediatR CQRS - 如何处理不存在的资源(asp.net core web api)

Posted

技术标签:

【中文标题】MediatR CQRS - 如何处理不存在的资源(asp.net core web api)【英文标题】:MediatR CQRS - How to deal with unexisting resources (asp.net core web api) 【发布时间】:2019-04-25 07:29:09 【问题描述】:

所以我最近开始学习如何将 MediatR 库与 ASP.NET Core Web API 一起使用,但我不确定如何在对一个 DELETE/PUT/PATCH 请求发出时返回 NotFound()不存在的资源。

如果我们以 DELETE 为例,这是我的控制器操作:

[HttpDelete("id")]
public async Task<IActionResult> Delete(int id)

    await Mediator.Send(new DeleteCourseCommand Id = id);

    return NoContent();

命令:

public class DeleteCourseCommand : IRequest

    public int Id  get; set; 

命令处理程序:

public class DeleteCourseCommandHandler : IRequestHandler<DeleteCourseCommand>

    private readonly UniversityDbContext _context;

    public DeleteCourseCommandHandler(UniversityDbContext context)
    
        _context = context;
    

    public async Task<Unit> Handle(DeleteCourseCommand request, CancellationToken cancellationToken)
    
        var course = await _context.Courses.FirstOrDefaultAsync(c => c.Id == request.Id, cancellationToken);


        if (course != null)
        
            _context.Courses.Remove(course);
            var saveResult = await _context.SaveChangesAsync(cancellationToken);
            if (saveResult <= 0)
            
                throw new DeleteFailureException(nameof(course), request.Id, "Database save was not successful.");
            
        

        return Unit.Value;
    

正如您在 Handle 方法中看到的那样,如果保存时出现错误,则会引发异常,从而导致 500 内部服务器错误(我相信这是正确的)。但是如果没有找到课程,我怎么能把它反馈给控制器上的动作呢?是否只是在控制器操作中调用查询以获取课程,然后如果它不存在则返回 NotFound() 或然后调用命令以删除课程?这当然可行,但在我经历过的所有示例中,我还没有遇到使用两个 Mediator 调用的 Action。

【问题讨论】:

什么是Unit @KirkLarkin 它是在 MediatR 库中定义的,但我仍在尝试自己解决这个问题,哈哈。我见过的很多命令示例都返回 Task. Unit 只是 void 返回类型的概念。您无法定义返回 void 的接口,因此创建了 Unit。 【参考方案1】:

MediatR 支持请求/响应模式,它允许您从处理程序类返回响应。要使用这种方法,您可以使用IRequest 的通用版本,如下所示:

public class DeleteCourseCommand : IRequest<bool>
    ...

在这种情况下,我们声明bool 将是响应类型。为简单起见,我在这里使用bool:我建议为您的最终实现使用更具描述性的内容,但bool 足以用于解释目的。

接下来,您可以更新您的 DeleteCourseCommandHandler 以使用这种新的响应类型,如下所示:

public class DeleteCourseCommandHandler : IRequestHandler<DeleteCourseCommand, bool>

    ...

    public async Task<bool> Handle(DeleteCourseCommand request, CancellationToken cancellationToken)
    
        var course = ...

        if (course == null)
            return false; // Simple example, where false means it wasn't found.

        ...

        return true;
    

正在实现的IRequestHandler 现在有两种通用类型,命令和响应。这需要更新Handle 的签名以返回bool 而不是Unit(在您的问题中,Unit 未被使用)。

最后,您需要更新您的 Delete 操作以使用新的响应类型,如下所示:

public async Task<IActionResult> Delete(int id)

    var courseWasFound = await Mediator.Send(new DeleteCourseCommand Id = id);

    if (!courseWasFound)
        return NotFound();

    return NoContent();

【讨论】:

请注意,HTTP DELETE 不应返回 NotFound。它应该是幂等的。无论资源在调用之前是否存在,它都应该返回 NoContent。 @YuliBonner 有一个 opinion 元素,但无论哪种方式,这个答案都使用 404,因为这就是 OP 所要求的。 @YuliBonner 幂等意味着多次调用后系统的状态不会改变。这并不意味着您不能向调用者发出资源是否存在的信号。【参考方案2】:

我喜欢从我的命令中返回事件。该命令告诉您的应用程序客户端想要它做什么。响应是它实际所做的。

顺便说一句——据说命令处理程序应该返回任何东西。这真的只有在完全异步的环境中才是正确的,在这个环境中,命令在响应客户端接受它之后的某个时间才会完成。在这种情况下,您将返回 Task&lt;Unit&gt; 并发布这些事件。一旦它们被提升,客户将通过其他渠道获得它们,例如 SignalR 集线器。无论哪种方式,事件都是告诉客户您的应用程序正在发生什么的最佳方式。

首先为您的事件定义一个接口

public interface IEvent



然后,为命令中可能发生的每件事创建事件。如果您想对这些信息做一些事情,您可以在其中包含信息,或者如果类本身就足够了,则将它们留空。

public class CourseNotFoundEvent : IEvent




public class CourseDeletedEvent : IEvent



现在,让你的命令返回一个事件接口。

public class DeleteCourseCommand : IRequest<IEvent>



您的处理程序看起来像这样:

public class DeleteCourseCommandHandler : IRequestHandler<DeleteCourseCommand, IEvent>

    private readonly UniversityDbContext _context;

    public DeleteCourseCommandHandler(UniversityDbContext context)
    
        _context = context;
    

    public async Task<IEvent> Handle(DeleteCourseCommand request, CancellationToken cancellationToken)
    
        var course = await _context.Courses.FirstOrDefaultAsync(c => c.Id == request.Id, cancellationToken);

        if (course is null) 
            return new CourseNotFoundEvent();

        _context.Courses.Remove(course);
        var saveResult = await _context.SaveChangesAsync(cancellationToken);
        if (saveResult <= 0)
        
            throw new DeleteFailureException(nameof(course), request.Id, "Database save was not successful.");
        

        return new CourseDeletedEvent();
    

最后,您可以在 Web API 上使用模式匹配来根据返回的事件执行操作。

[HttpDelete("id")]
public async Task<IActionResult> Delete(int id)

    var @event = await Mediator.Send(new DeleteCourseCommand Id = id);

    if(@event is CourseNotFoundEvent)
        return NotFound();

    return NoContent();

【讨论】:

【参考方案3】:

通过我找到的更多示例,我设法解决了我的问题。解决方案是定义自定义异常,例如 NotFoundException,然后将其抛出到 Query/Command Handler 的 Handle 方法中。那么为了让 MVC 能够恰当地处理这个问题,需要一个 ExceptionFilterAttribute 的实现来决定如何处理每个 Exception:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute

    public override void OnException(ExceptionContext context)
    
        if (context.Exception is ValidationException)
        
            context.HttpContext.Response.ContentType = "application/json";
            context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            context.Result = new JsonResult(
                ((ValidationException)context.Exception).Failures);

            return;
        

        var code = HttpStatusCode.InternalServerError;

        if (context.Exception is NotFoundException)
        
            code = HttpStatusCode.NotFound;
        

        context.HttpContext.Response.ContentType = "application/json";
        context.HttpContext.Response.StatusCode = (int)code;
        context.Result = new JsonResult(new
        
            error = new[]  context.Exception.Message 
        );
    

启动类:

services.AddMvc(options => options.Filters.Add(typeof(CustomExceptionFilterAttribute)));

自定义异常:

public class NotFoundException : Exception

    public NotFoundException(string entityName, int key)
        : base($"Entity entityName with primary key key was not found.")
       
    

然后在Handle方法中:

if (course != null)

    _context.Courses.Remove(course);
    var saveResult = await _context.SaveChangesAsync(cancellationToken);
    if (saveResult <= 0)
    
        throw new DeleteFailureException(nameof(course), request.Id, "Database save was not successful.");
    

else

    throw new NotFoundException(nameof(Course), request.Id);


return Unit.Value;

这似乎可以解决问题,如果有人可以看到任何潜在问题,请告诉我!

【讨论】:

以上是关于MediatR CQRS - 如何处理不存在的资源(asp.net core web api)的主要内容,如果未能解决你的问题,请参考以下文章

MediatR 和 CQRS 测试。如何验证调用了该处理程序?

ABP CQRS 实现案例:基于 MediatR 实现

.NET 5 源代码生成器——MediatR——CQRS

NTILE() 如何处理不平衡的数据?

如何处理不受信任的服务 URL?

如何处理不安全的 XMLHttpRequest 端点 [重复]