MediatR CQRS - как работать с несуществующими ресурсами (asp.net core web api)

Итак, я недавно начал узнавать об использовании библиотеки MediatR с веб-API ASP.NET Core, и я не уверен, как вернуть NotFound (), когда запрос DELETE / PUT / PATCH был сделан для несуществующего ресурса.

Если мы возьмем, например, 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 (), если он не существует, или последующего вызова команды для УДАЛЕНИЯ курса? Это, конечно, сработает, но из всех примеров, которые я рассмотрел, я не встречал Action, использующего два вызова Mediator.


person rejy11    schedule 22.11.2018    source источник
comment
Что такое Unit?   -  person Kirk Larkin    schedule 22.11.2018
comment
@KirkLarkin Он определен в библиотеке MediatR, но я все еще пытаюсь понять это сам, ха-ха. Многие примеры команд, которые я видел, возвращают Task ‹Unit›.   -  person rejy11    schedule 22.11.2018
comment
Единица - это просто концепция типа возвращаемого значения void. Вы не можете определить интерфейс для возврата void, поэтому был создан модуль.   -  person Todd Skelton    schedule 20.12.2018


Ответы (3)


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();
}
person Kirk Larkin    schedule 22.11.2018
comment
Просто примечание, HTTP DELETE не должен возвращать NotFound. Он должен быть идемпотентным. Он должен возвращать NoContent независимо от того, существовал ли ресурс до вызова или нет. - person Yuli Bonner; 20.12.2018
comment
@YuliBonner В этом есть элемент мнения, но в любом случае в этом ответе используется 404, поскольку это то, о чем просил OP. - person Kirk Larkin; 20.12.2018
comment
Идемпотентность @YuliBonner означает, что состояние системы после нескольких вызовов не изменяется. Это не означает, что вы не можете сигнализировать вызывающему, существует ли ресурс или нет. - person drizin; 27.12.2020

Мне нравится возвращать события из моих команд. Команда сообщает вашему приложению, чего хочет клиент. Ответ - это то, что он сделал на самом деле.

Кстати, сказано, что обработчики команд должны что-нибудь возвращать. На самом деле это верно только в полностью асинхронной среде, где команда не будет завершена до тех пор, пока клиенту не ответит, что она принята. В этом случае вы должны вернуть 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();
    }
}

Наконец, вы можете использовать сопоставление с образцом в своем веб-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();
}
person Todd Skelton    schedule 20.12.2018

Мне удалось решить мою проблему с помощью еще нескольких примеров, которые я нашел. Решение состоит в том, чтобы определить настраиваемые исключения, такие как NotFoundException, а затем бросить это в метод 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 }
        });
    }
}

Класс запуска:

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;

Кажется, это помогает, если кто-то видит какие-либо потенциальные проблемы с этим, пожалуйста, дайте мне знать!

person rejy11    schedule 22.11.2018