MediatR CQRS - как работать с несуществующими ресурсами (основной веб-интерфейс asp.net) - PullRequest
0 голосов
/ 22 ноября 2018

Итак, я недавно начал изучать использование библиотеки MediatR с ASP.NET Core Web API, и я не уверен, что делать с возвратом 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 сервера (что, я считаю, является правильным).Но если Курс не найден, как я могу передать это в Действие на Контроллере?Является ли это просто случаем вызова запроса для получения курса в действии контроллера, а затем возврата NotFound (), если он не существует, или затем вызова команды для удаления курса?Это, конечно, сработает, но из всех примеров, через которые я прошел, я не сталкивался с действием, которое использует два вызова посредника.

Ответы [ 3 ]

0 голосов
/ 22 ноября 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;

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

0 голосов
/ 20 декабря 2018

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

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

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