In our MVC application all of our read actions as a paramter take a query which implements:
public interface IQuery<out TResponse> { }
Within the action the query is passed to a bus which locates a handler and returns a view model. So controllers now look something like this:
public ActionResult Edit(DetailsQuery query)
{
var model = mediator.Request(query);
return View(model);
}
Effectively just passing queries to our mediator and returning the result. We have hundreds of actions that look like this. There is the odd action that does something conditional (which I would leave as they are) but the rest are just the same boilerplate again and again. We have over hundred different queries
How can I refactor this to something more explicit? I guess moving to a Model View Query Handler rather than the boilerplate controller action that just hands off query to the bus and returns model to View.
What extension points should I look at in MVC? Effectively instead of having to write the action handler - just have some automatic way of wiring together strongly typed query and getting back the correct ViewModel.
If I can? Should I? I just don't like seeing hundreds of actions that all look the same.
First, thanks for the link to the post "Put your controllers on a diet: GETs and queries". My code example uses types from it.
My solution also involves usage of action filters as point to inject generic behaviour.
Controller is simple enough and looks like @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();
}
}
Working on QueryFilterAttribute
I realised that IMediator
and its implementation can be omitted. It is enough to know type of query to resolve an instance of IQueryHandler<,>
via IoC.
In my example Castle Windsor and implementation of 'Service Locator' pattern are used.
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
interface is added to avoid working with 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);
}
}
Types should be registered in Global.asax.cs
container.Register(Component.For<ISession>().ImplementedBy<FakeSession>());
container.Register(
Classes.FromThisAssembly()
.BasedOn(typeof(IQueryHandler<,>))
.WithService.Base()
.LifestylePerWebRequest());
There is a link to gist on github with all code.
Sounds to me like you want a custom ControllerActionInvoker e.g.
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
};
}
}
We then introduce a base controller where we inject our custom 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);
}
}
Then finally in our controller, we derive from our base and return the query information from our actions e.g.
public class QueryController : BaseReadController
{
// our actions now do nothing but define a route for our queries
public void About(DetailsQuery query)
{
}
}
What you effectively end up with here is bodiless actions so you lose the repetitive code but, in my opinion, you sacrifice some readability (there is a lot of voodoo happening in the controller now which isn't immediately obvious).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With