Могу ли я реорганизовать обработчик запросов вида модели?
В нашем приложении MVC все наши действия чтения в качестве параметра принимают запрос, который реализует:
public interface IQuery<out TResponse> { }
В рамках действия запрос передается на шину, которая находит обработчик и возвращает модель представления. Поэтому контроллеры теперь выглядят примерно так:
public ActionResult Edit(DetailsQuery query)
{
var model = mediator.Request(query);
return View(model);
}
Эффективно просто передавать запросы нашему посреднику и возвращать результат. У нас есть сотни действий, которые выглядят так. Есть странное действие, которое делает что-то условное (которое я бы оставил, как есть), но все остальное - это один и тот же шаблон снова и снова. У нас более ста разных запросов
Как я могу реорганизовать это на что-то более явное? Я предполагаю, что вы перейдете к обработчику запросов вида Model View, а не к действию контроллера шаблона, который просто передает запрос на шину и возвращает модель View.
Какие точки расширения следует искать в MVC? Эффективно вместо того, чтобы писать обработчик действий - просто используйте некоторый автоматический способ подключения вместе строго типизированного запроса и возврата правильной ViewModel.
Если смогу? Нужно ли мне? Мне просто не нравится видеть сотни действий, которые выглядят одинаково.
Ответы
Ответ 1
Во-первых, спасибо за ссылку на сообщение Поставьте свои контроллеры на диету: GET и запросы. В моем примере кода используются типы из него.
Мое решение также включает использование фильтров действий в качестве точки для введения общего поведения.
Контроллер достаточно прост и похож на @Kambiz Shahim's:
[QueryFilter]
public class ConferenceController : Controller
{
public ActionResult Index(IndexQuery query)
{
return View();
}
public ViewResult Show(ShowQuery query)
{
return View();
}
public ActionResult Edit(EditQuery query)
{
return View();
}
}
Работая над QueryFilterAttribute
, я понял, что IMediator
и его реализация может быть опущена. Достаточно знать тип запроса для разрешения экземпляра IQueryHandler<,>
через IoC.
В моем примере Castle Windsor и используется "Locator" .
public class QueryFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
object query = filterContext.ActionParameters["query"];
Type queryType = query.GetType();
Type modelType = queryType.GetInterfaces()[0].GetGenericArguments()[0];
var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryType, modelType);
// Here you should resolve your IQueryHandler<,> using IoC
// 'Service Locator' pattern is used as quick-and-dirty solution to show that code works.
var handler = ComponentLocator.GetComponent(handlerType) as IQueryHandler;
var model = handler.Handle(query);
filterContext.Controller.ViewData.Model = model;
}
}
IQueryHandler
добавлен интерфейс, чтобы избежать работы с Reflection
/// <summary>
/// All derived handlers can be refactored using generics. But in the real world handling logic can be completely different.
/// </summary>
/// <typeparam name="TQuery">The type of the query.</typeparam>
/// <typeparam name="TResponse">The type of the response.</typeparam>
public interface IQueryHandler<in TQuery, out TResponse> : IQueryHandler
where TQuery : IQuery<TResponse>
{
TResponse Handle(TQuery query);
}
/// <summary>
/// This interface is used in order to invoke 'Handle' for any query type.
/// </summary>
public interface IQueryHandler
{
object Handle(object query);
}
/// <summary>
/// Implements 'Handle' of 'IQueryHandler' interface explicitly to restrict its invocation.
/// </summary>
/// <typeparam name="TQuery">The type of the query.</typeparam>
/// <typeparam name="TResponse">The type of the response.</typeparam>
public abstract class QueryHandlerBase<TQuery, TResponse> : IQueryHandler<TQuery, TResponse>
where TQuery : IQuery<TResponse>
{
public abstract TResponse Handle(TQuery query);
object IQueryHandler.Handle(object query)
{
return Handle((TQuery)query);
}
}
Типы должны быть зарегистрированы in Global.asax.cs
container.Register(Component.For<ISession>().ImplementedBy<FakeSession>());
container.Register(
Classes.FromThisAssembly()
.BasedOn(typeof(IQueryHandler<,>))
.WithService.Base()
.LifestylePerWebRequest());
Существует ссылка на github со всем кодом.
Ответ 2
Звучит так, как будто вы хотите создать ControllerActionInvoker, например.
public class ReadControllerActionInvoker : ControllerActionInvoker
{
private IMediator mediator;
public ReadControllerActionInvoker(IMediator mediator)
{
this.mediator = mediator;
}
protected override ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
{
ViewDataDictionary model = null;
// get our query parameter
var query = GetParameterValue(controllerContext, actionDescriptor.GetParameters().Where(x => x.ParameterName == "query").FirstOrDefault());
// pass the query to our mediator
if (query is DetailsQuery)
model = new ViewDataDictionary(this.mediator.Request((DetailsQuery)query));
// return the view with read model returned from mediator
return new ViewResult
{
ViewName = actionDescriptor.ActionName,
ViewData = model
};
}
}
Затем мы вводим базовый контроллер, в который вводим пользовательский ControllerActionInvoker
public class BaseReadController : Controller
{
protected IMediator Mediator { get; set; }
protected override void Initialize(System.Web.Routing.RequestContext requestContext)
{
base.Initialize(requestContext);
ActionInvoker = new ReadControllerActionInvoker(Mediator);
}
}
Затем, наконец, в нашем контроллере мы выводим из нашей базы и возвращаем информацию запроса из наших действий, например.
public class QueryController : BaseReadController
{
// our actions now do nothing but define a route for our queries
public void About(DetailsQuery query)
{
}
}
То, что вы в итоге получаете здесь, - это бестелесные действия, поэтому вы теряете повторяющийся код, но, на мой взгляд, вы жертвуете некоторой удобочитаемостью (сейчас в контроллере происходит много вуду, что не сразу становится очевидным).
Ответ 3
Другим решением является создание ActionFilter
для украшения действий в таких контроллерах, как это:
[GenericActionFilter(ModelType=typeof(ShowModel))]
public ActionResult Edit(ShowQuery query)
{
return View();
}
и это ActionFilter
public class GenericActionFilter : ActionFilterAttribute
{
public Type ModelType { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
IMediator mediator = null;
if(filterContext.Controller is BaseController)
{
mediator = ((BaseController)filterContext.Controller).GetMediator();
object paramValue = filterContext.ActionParameters["query"];
var method = mediator.GetType().GetMethod("Request").MakeGenericMethod(new Type[] { ModelType });
var model = method.Invoke(mediator, new object[] { paramValue });
filterContext.Controller.ViewData.Model = model;
}
}
}
и BaseController
public class BaseController : Controller
{
private readonly IMediator mediator;
public BaseController():this(new Mediator())
{
}
public BaseController(IMediator mediator)
{
this.mediator = mediator;
}
public IMediator GetMediator()
{
return mediator;
}
}
это основано на предположении, что метод Request
Mediator
является общим методом, подобным этому:
public interface IMediator
{
TResponse Request<TResponse>(IQuery<TResponse> query);
}
и
public class ShowQuery : IQuery<ShowModel>
{
public string EventName { get; set; }
}