c# asp.net-mvc asp.net-mvc-4 architecture cqrs

c# - ¿Puedo refactorizar a Model View Query Handler?



asp.net-mvc asp.net-mvc-4 (3)

En nuestra aplicación MVC, todas nuestras acciones de lectura como parámetro toman una consulta que implementa:

public interface IQuery<out TResponse> { }

Dentro de la acción, la consulta se pasa a un bus que localiza un controlador y devuelve un modelo de vista. Entonces los controladores ahora se ven algo como esto:

public ActionResult Edit(DetailsQuery query) { var model = mediator.Request(query); return View(model); }

De hecho, simplemente pasando consultas a nuestro mediador y devolviendo el resultado. Tenemos cientos de acciones que se ven así. Existe una acción extraña que hace algo condicional (que dejaría tal como está) pero el resto es el mismo repetitivo una y otra vez. Tenemos más de cien consultas diferentes

¿Cómo puedo refactorizar esto para algo más explícito? Supongo que pasar a un Manejador de consultas de Model View en lugar de la acción del controlador repetitivo que simplemente pasa de la consulta al bus y devuelve el modelo a View.

¿Qué puntos de extensión debería considerar en MVC? Efectivamente, en lugar de tener que escribir el manejador de acciones, solo tiene una forma automática de cablear una consulta fuertemente tipada y obtener el modelo de vista correcto.

¿Si puedo? ¿Debería? Simplemente no me gusta ver cientos de acciones que se ven todas iguales.


En primer lugar, gracias por el enlace a la publicación lostechies.com/jimmybogard/2013/10/29/… . Mi ejemplo de código usa tipos de él.

Mi solución también implica el uso de filtros de acción como punto para inyectar un comportamiento genérico.

El controlador es lo suficientemente simple y se parece a @Kambiz Shahim:

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

Al trabajar en QueryFilterAttribute me di cuenta de que IMediator y su implementación se pueden omitir. Es suficiente conocer el tipo de consulta para resolver una instancia de IQueryHandler<,> través de IoC.

En mi ejemplo, Castle Windsor y la implementación del patrón ''Localizador de servicios'' se utilizan.

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 interfaz IQueryHandler para evitar trabajar con 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); } }

Los tipos deben registrarse in Global.asax.cs

container.Register(Component.For<ISession>().ImplementedBy<FakeSession>()); container.Register( Classes.FromThisAssembly() .BasedOn(typeof(IQueryHandler<,>)) .WithService.Base() .LifestylePerWebRequest());

Hay un enlace para hacer github con todo el código.


Me parece que quieres una ControllerActionInvoker personalizada, por ejemplo

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 }; } }

Luego presentamos un controlador base donde inyectamos nuestra ControllerActionInvoker personalizada

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

Finalmente, en nuestro controlador, derivamos de nuestra base y devolvemos la información de consulta de nuestras acciones, por ejemplo

public class QueryController : BaseReadController { // our actions now do nothing but define a route for our queries public void About(DetailsQuery query) { } }

Lo que efectivamente terminas aquí son las acciones sin cuerpo, así que pierdes el código repetitivo pero, en mi opinión, sacrificas algo de legibilidad (ahora hay muchos vudú sucediendo en el controlador, lo cual no es inmediatamente obvio).


Otra solución es crear un ActionFilter para decorar las acciones en los controladores como este:

[GenericActionFilter(ModelType=typeof(ShowModel))] public ActionResult Edit(ShowQuery query) { return View(); }

y este es el 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; } } }

y el 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; } }

esto se basa en la suposición de que el método de Request del Mediator es un método genérico como este:

public interface IMediator { TResponse Request<TResponse>(IQuery<TResponse> query); }

y

public class ShowQuery : IQuery<ShowModel> { public string EventName { get; set; } }