当前位置: 动力学知识库 > 问答 > 编程问答 >

c# - Can I refactor to Model View Query Handler?

问题描述:

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).

网友答案:

Another solution is to create an ActionFilter to decorate the actions in the controllers like this:

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

and this is the 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;
        }
    }

}

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

this is based on an assumption which the Request method of the Mediator is a generic method like this:

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

and

public class ShowQuery  : IQuery<ShowModel>
{
    public string EventName { get; set; }
}
分享给朋友:
您可能感兴趣的文章:
随机阅读: