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();
}
所以我最近开始学习如何将 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();
}