Как перенести обработку валидации с действия контроллера на декоратора

Редактирование обслуживания

После некоторого использования этого подхода я обнаружил, что добавляю только один и тот же код шаблона в каждом контроллере, поэтому решил сделать магию отражения. Тем временем я бросил с использованием MVC для своих просмотров - Razor просто так скучен и уродлив, поэтому я в основном использую мои обработчики как бэкэнд JSON. Подход, который я использую в настоящее время, заключается в том, чтобы украсить мои запросы/команды атрибутом Route, который находится в некоторой общей сборке, например:

[Route("items/add", RouteMethod.Post)]
public class AddItemCommand { public Guid Id { get; set; } }

[Route("items", RouteMethod.Get)]
public class GetItemsQuery : IQuery<GetItemsResponse> { }

// The response inherits from a base type that handles
// validation messages and the like
public class GetItemsResponse : ServiceResponse { }

Затем я реализовал хост MVC, который извлекает аннотированные команды/запросы и генерирует контроллеры и обработчики для меня во время запуска. Таким образом, моя логика приложения, наконец, свободна от MVC cruft. Ответы на запросы также автоматически заполняются сообщениями проверки. Мои приложения MVC теперь выглядят следующим образом:

+ MvcApp
  +- Global.asax
  +- Global.asax.cs - Startup the host and done
  +- Web.config

Поняв, что я действительно не использую MVC за пределами хоста, и постоянно испытываю проблемы с зависимостями bazillion, которые имеют инфраструктура, я реализовал другой хост на основе NServiceKit. Ничто не должно меняться в моей логике приложения, а зависимости ниже System.Web, NServiceKit и NServiceKit.Text, которые хорошо заботятся о привязке модели. Я знаю, что это очень похожий подход к тому, как NServiceKit/ServiceStack делает свои вещи, но теперь я полностью отключаюсь от используемой веб-фреймворки, поэтому, если будет лучше, я просто реализую другой хост и что он.

Ситуация

В настоящее время я работаю над сайтом ASP.NET MVC, который реализует разделение бизнес-вида через IQueryHandler и ICommandHandler абстракции (используя всемогущий SimpleInjector для инъекции зависимостей).

Проблема

Мне нужно приложить некоторую пользовательскую логику проверки к QueryHandler через декоратор и работать очень хорошо само по себе. Проблема в том, что в случае ошибок проверки я хочу иметь возможность показать тот же вид, что действие вернулось бы, но с информацией об ошибке проверки, конечно. Вот пример для моего случая:

public class HomeController : Controller
{
    private readonly IQueryHandler<SomeQuery, SomeTransport> queryHandler;

    public ActionResult Index()
    {
        try
        {
            var dto = this.queryHandler.Handle(new SomeQuery { /* ... */ });
            // Doing something awesome with the data ...
            return this.View(new HomeViewModel());
        }
        catch (ValidationException exception)
        {
            this.ModelState.AddModelErrors(exception);
            return this.View(new HomeViewModel());
        }
    }
}

В этом случае у меня есть некоторая бизнес-логика, обрабатываемая QueryHandler, которая украшена ValidationQueryHandlerDecorator, которая бросает ValidationException, когда это уместно.

Что я хочу делать

Я хочу что-то вроде:

public class HomeController : Controller
{
    private readonly IQueryHandler<SomeQuery, SomeTransport> queryHandler;

    public ActionResult Index()
    {
        var dto = this.queryHandler.Handle(new SomeQuery { /* ... */ });
        // Doing something awesome with the data ...
        // There is a catch-all in place for unexpected exceptions but
        // for ValidationExceptions I want to do essentially the same
        // view instantiation but with the model errors attached
        return this.View(new HomeViewModel());
    }
}

Я думал о специальном ValidationErrorHandlerAttribute, но потом я теряю контекст, и я не могу действительно вернуть правильный вид. То же самое касается подхода, в котором я просто обертываю IQueryHandler<,> декоратором... Я видел некоторые странные фрагменты кода, которые выполняли некоторую струйку на маршруте, а затем создавали экземпляр нового контроллера и viewmodel через Activator.CreateInstance - это не кажется хорошей идеей.

Так что мне интересно, есть ли хороший способ сделать это... может быть, я просто не вижу дерева с деревьев. Спасибо!

Ответ 1

Я не думаю, что есть способ заставить метод действия не обращать на это внимания, поскольку метод действия управляет возвращаемой моделью представления, а в случае исключения проверки нужно вернуть модель представления со всеми фактические данные (чтобы пользователь не потерял свои изменения). Однако, что бы вы могли сделать, чтобы сделать это более удобным, добавьте метод расширения для выполнения запросов в действии:

public ActionResult Index()
{
    var result = this.queryHandler.ValidatedHandle(this.ModelState, new SomeQuery { });

    if (result.IsValid) {
        return this.View(new HomeViewModel(result.Data));
    }
    else
    {
        return this.View(new HomeViewModel());
    }
}

Метод расширения ValidatedHandle может выглядеть следующим образом:

public static ValidatedResult<TResult> ValidatedHandle<TQuery, TResult>(
    this IQueryHandler<TQuery, TResult> handler,
    TQuery query, ModelStateDictionary modelState)
{
    try
    {
        return new ValidatedResult<TResult>.CreateValid(handler.Handle(query));
    }
    catch (ValidationException ex)
    {
        modelState.AddModelErrors(ex);
        return ValidatedResult<TResult>.Invalid;
    }
}

Обратите внимание, что вы должны улавливать только такое исключение проверки, если проверка включена в данные, введенные пользователем. Если вы отправляете запрос с параметрами, которые установлены программно, исключение проверки просто означает ошибку программирования, и вы должны вести блог, регистрировать исключение и показывать пользователю дружественную страницу ошибок.