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 库中定义的,但我仍在尝试自己解决这个问题,哈哈。我见过的很多命令示例都返回 TaskMediatR 支持请求/响应模式,它允许您从处理程序类返回响应。要使用这种方法,您可以使用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<Unit>
并发布这些事件。一旦它们被提升,客户将通过其他渠道获得它们,例如 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)的主要内容,如果未能解决你的问题,请参考以下文章