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

MediatR CQRS - How to deal with unexisting resources (asp.net core web api)

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

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

[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 内部服务器错误(我相信这是正确的)。但是,如果找不到课程,我该如何将其反馈给控制器上的操作?是否只是调用查询以获取控制器操作中的课程,然后 return NotFound() 如果它不存在或调用命令以删除课程的情况?这当然可行,但在我经历过的所有示例中,我还没有遇到使用两个 Mediator 调用的 Action。

MediatR 支持 Request/Response 模式,它允许您 return 来自处理程序 class 的响应。要使用此方法,您可以使用 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 的签名更新为 return a 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();
}

我通过找到的更多示例设法解决了我的问题。解决办法是定义自定义的Exception比如NotFoundException,然后在Query/CommandHandler的Handle方法中抛出这个。然后为了让 MVC 适当地处理这个,需要一个 ExceptionFilterAttribute 的实现来决定如何处理每个异常:

[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 }
        });
    }
}

启动Class:

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;

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

我喜欢 return从我的命令中获取事件。该命令告诉您的应用程序客户希望它做什么。响应是它实际所做的。

顺便说一句——据说命令处理程序应该 return 任何东西。只有在完全异步的环境中才会如此,在这种环境中,命令要等到对客户端的响应被接受后的某个时间才能完成。在这种情况下,您将 return Task<Unit> 并发布这些事件。一旦它们被提升,客户端将通过其他渠道获得它们,例如 SignalR hub。无论哪种方式,事件都是告诉客户您的应用程序中发生的事情的最佳方式。

首先为您的活动定义一个界面

public interface IEvent
{

}

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

public class CourseNotFoundEvent : IEvent
{

}

public class CourseDeletedEvent : IEvent
{

}

现在,让您的命令 return 成为一个事件接口。

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();
    }
}

最后,您可以在网络上使用模式匹配 API 来根据获得 returned 的事件做事。

[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();
}