Tuesday, November 9, 2010

When just a query don't work - MappedQueryActionResult

So in my last post I made a Query action result. This works great for list and details screens. What about edit screens though? If you're posting back your actual entity as your viewmodel this works fine. However, I've gotten to where I make nice little small view models that represent just the form I'm wanting to post. Like this method
   1:   public ActionResult Edit(string email)
   2:          {
   3:              var readModel = _detailsReadModelRepository.GetSingle(d=>d.EmailAddress == email);
   4:              var viewModel = new EditMartialArtistForm(readModel);
   5:              return View(viewModel);
   6:   
   7:          }

Nothing wrong with this method. Can't be - because I wrote it. *snicker. But, I think I can make it better. So, since I'm not big on solving my own problems, I went and looked at some of Jimmy Bogard's stuff. Jimmy wrote AutoMapper, and is an all round genius. In fact, the only reason you should be reading this blog is because you've read all his blogs and you're not geek full yet. I pulled down the code from his mvcConf at http://headspringlabs.codeplex.com/ and found this little gem.



   1:  using System;
   2:  using System.Web.Mvc;
   3:  using AutoMapper;
   4:   
   5:  namespace HeadspringExample.UI.Helpers
   6:  {
   7:      public class AutoMapViewResult : ActionResult
   8:      {
   9:          public Type SourceType { get; private set; }
  10:          public Type DestinationType { get; private set; }
  11:          public ViewResult View { get; private set; }
  12:   
  13:          public AutoMapViewResult(Type sourceType, Type destinationType, ViewResult view)
  14:          {
  15:              SourceType = sourceType;
  16:              DestinationType = destinationType;
  17:              View = view;
  18:          }
  19:   
  20:          public override void ExecuteResult(ControllerContext context)
  21:          {
  22:              var model = Mapper.Map(View.ViewData.Model, SourceType, DestinationType);
  23:   
  24:              View.ViewData.Model = model;
  25:   
  26:              View.ExecuteResult(context);
  27:          }
  28:      }
  29:  }

If you don't know what AutoMapper is, well, that's not the point of this blog, but basically, it maps an instance of one type to an instance of another based on conventions. Saves LOTS and lots of boilerplate code that we used to hire interns to write before Bush drove the Economy in a ditch and Obama backed a tow truck over it trying to fix it. Now we just go steal Jimmy's stuff and it gets done for free and interns work at McDonalds and McDonalds workers go beg on the street corner and panhandlers are just SOL.  What the heck was that all about???  God I love Dos Equis!

Back on track. Yeah, it's a good beer!

Ok, so I wanna send a Query off to an AutoMapper then send that off to a view. Want my consumers to look something like this.


   1:   public ActionResult Edit(string email)
   2:          {
   3:              return MappedQueryView<GetMartialArtistDetailsByEmail,EditMartialArtistForm>(s =>s.EmailAddress = email);
   4:          }

Uhh... I don't know if that's even possible, since IQuery.Execute is object... But hey, lets see what we can come up with.  25 minutes (and 2 more Dos Equis) later... Duhhh... Look at Jimmy's AutoMapViewResult. He made overloads for IMappingEngine.Map that take source, source type and destination type (not the generics).. We're in business! So I wrote this.

   1:  using System;
   2:   
   3:  using System.Web.Mvc;
   4:  using AutoMapper;
   5:  using myDojo.Infrastructure.CQRS.Query;
   6:  using StructureMap;
   7:   
   8:  namespace myDojo.Infrastructure.Web
   9:  {
  10:      public class MappedQueryViewResult<TQuery,TDestination> : ViewResult where TQuery:IQuery
  11:      {
  12:          private static IMappingEngine Mapper { get { return Container.GetInstance<IMappingEngine>(); } }
  13:          private static IContainer Container { get { return ServiceLocation.CurrentContainer; } }
  14:   
  15:          public MappedQueryViewResult(Action<TQuery> buildUpQuery)
  16:          {
  17:              BuildUpQuery = buildUpQuery;
  18:          }
  19:   
  20:          protected Action<TQuery> BuildUpQuery { get; set; }
  21:   
  22:          public override void ExecuteResult(ControllerContext context)
  23:          {
  24:              var query = Container.GetInstance<TQuery>();
  25:              if (BuildUpQuery != null)
  26:                  BuildUpQuery(query);
  27:              var source = query.Execute();
  28:              var viewModel = Mapper.Map(source,source.GetType(), typeof (TDestination));
  29:              ViewData.Model = viewModel;
  30:              base.ExecuteResult(context);
  31:          }
  32:          
  33:      }
  34:  }


Not much to it really. I just use go get the Query from StructureMap, build it up (exactly the same way QueryActionResult does) but instead of setting the Model to the query results, I pump it through the IMappingEngine. Add a nice little Convenience method to our base controller.

   1:  public ActionResult MappedQuery<TQuery,TDestination>(Action<TQuery> buildUpQuery=null) where TQuery:IQuery
   2:          {
   3:              return new MappedQueryViewResult<TQuery, TDestination>(buildUpQuery);
   4:          }
 
And there we go! The method now looks like this.
 

   1:  public ActionResult Edit(string email)
   2:          {
   3:              return MappedQuery<GetMartialArtistDetailsByEmail,EditMartialArtistForm>(s =>s.EmailAddress = email);
   4:          }
A nice little side effect to not depending on the repositories directly is that we no longer have ANY direct dependencies in the controller now. Default ctor is fine. All the service location stuff happens in the ActionResult. Personally, I think this is a good thing. I can test my Queries in isolation and have the infrastructure wrapped up well and we know it works. 
Here's what the controller looks like now.


   1:  using System;
   2:  using System.Web.Mvc;
   3:  using myDojo.Commands.Users;
   4:  using myDojo.Infrastructure.Web;
   5:  using MyDojo.Query.Queries;
   6:  using myDojo.Web.Models;
   7:   
   8:  namespace myDojo.Web.Controllers
   9:  {
  10:      public class UserController : DefaultController
  11:      {
  12:          public ActionResult Register()
  13:          {
  14:              return View(new RegisterUserForm());
  15:          }
  16:          [HttpPost]
  17:          public CommandActionResult<RegisterUser> Register(RegisterUserForm form)
  18:          {
  19:              return Command(new RegisterUser(form.EmailAddress, null), () => RedirectToAction("Edit", new {email = form.EmailAddress}));
  20:          }
  21:          public ActionResult Edit(string email)
  22:          {
  23:              return MappedQuery<GetMartialArtistDetailsByEmail,EditMartialArtistForm>(s =>s.EmailAddress = email);
  24:          }
  25:          public ActionResult Details(Guid id)
  26:          {
  27:              return Query<MartialArtistDetailsById>(q => q.Id = id);
  28:          }
  29:          [HttpPost]
  30:          public CommandActionResult<EditMartialArtistInfo> Edit(EditMartialArtistForm model)
  31:          {
  32:              return Command(new EditMartialArtistInfo(model.Id, model.Name, model.Biography), () => RedirectToAction("List"));
  33:          }
  34:          public ActionResult List()
  35:          {
  36:              return Query<AllMatialArtists>();
  37:          }
  38:   
  39:      }
  40:   
  41:     
  42:  }
 

No comments:

Post a Comment