How I limit my abstractions in fubuMVC

In my previous post I discussed my own design of Controller Messages that I had previously used in ASP.NET MVC to force single responsibility principles and to ensure that I didn’t have any fat controllers.

My conclusion from that post was that I no longer need these handler abstractions now that I am using fubuMVC and I’ll attempt to illustrate what I pinched from the post that started all this thought process, namely Limit your abstractions: And how do you handle testing?

That post shows the provision of Commands and Queries on the controller to do away with unnecessary abstractions, and it occurred to me that the fubuMVC action is itself essentially a command, one of a series of behaviours/nodes that executes in a Russian doll model – I therefore can do away with the handler abstraction and treat the fubuMVC action/controller as a command.
All I need to do it provide the query mechanism on a base controller:

    public abstract class ControllerWithASession
    {
        protected ISession Session
        {
            get { return SessionProvider(); }
        }

        public Func<ISession> SessionProvider { get; set; }

        protected T Query<T>(Query<T> query)
        {
            if (AlternativeQueriesToRun.Any())
                return RunAlternativeQuery(query);

            query.Session = Session;
            return query.Execute();
        }

        T RunAlternativeQuery<T>(Query<T> query)
        {
            var alternate = AlternativeQueriesToRun.Where(q => 
                        q.GetType().IsAssignableFrom(typeof(Func<Query<T>, T>)))
                    .FirstOrDefault();
            if (alternate == null)
                throw new Exception(string.Format("Alternative queries were set up, 
                        but there was one missing for: {0}", query.GetType().Name));
            
            return (alternate as Func<Query<T>, T>).Invoke(query);
        }

        List<object> AlternativeQueriesToRun = new List<object>();
        
         public void AddAlternativeQueryToRun(object alternativeQuery)
         {
             AlternativeQueriesToRun.Add(alternativeQuery);
         }
    }

This new protected method Query<T> can run a query, but first checks if any alternatives have been set up – in my tests I’m therefore free to provide different implementations without the need for mocking an IRepository, IDataObtainer or other needless abstraction.
It also ensures that if any alternatives have been provided an exception is thrown if you don’t provide every query in the test.

The queries are concrete implementations of a generic abstraction:

    public class AssignmentPendingApprovalQuery : Query<AssignmentPendingApproval>
    {
        readonly int _userId;

        public AssignmentPendingApprovalQuery(int userId)
        {
            _userId = userId;
        }

        public override AssignmentPendingApproval Execute()
        {
            return //implementation here;
        }
    }

    public abstract class Query<T>
    {
        public ISession Session { get; set; }

        public abstract T Execute();
    }

So with all that set up, I can now write tests like this:

    [Subject(typeof(PostLoginDeciderController))]
    public class when_deciding_what_to_do_after_login_and_the_user_has_no_approved_request_
                                       and_no_assignment_approvals : TestingAController
    {
        Establish context = () =>
        {
            _user = User.ForId(8);
            _theRequestsCurrentUser = new TheRequestsCurrentUser {User = _user};

            _postLoginDeciderController = WithASession(new PostLoginDeciderController());
            SetupQuery<RegistrationPendingApproval>(query => null);
            SetupQuery<AssignmentPendingApproval>(query => null);
        };
        
        Because of = () => _continuation = 
                        _postLoginDeciderController.DecideWhereToGoAfterLogin(_theRequestsCurrentUser);

        It should_redirect_the_user_to_the_second_step_of_registration = () =>
            _continuation.AssertWasRedirectedTo<RegistrationController>(r => r.StepTwo());

        static PostLoginDeciderController _postLoginDeciderController;
        static FubuContinuation _continuation;
    }

The WithASession method on the base test class ensures that the controller is set up with a mocked session and I have the ability to set up queries, here explicitly returning null for both queries because a registration process is not complete.

Here is the base test class:

    public abstract class TestingAController
    {
        protected static Mock<ISession> Session { get; set; }

        protected static ControllerWithASession CurrentController { get; set; }

        protected static T WithASession<T>(T controller) where T : ControllerWithASession
        {
            CurrentController = controller;

            Session = new Mock<ISession>();
            controller.SessionProvider = () => Session.Object;
            return controller;
        }

        public static void SetupQuery<T>(Func<Query<T>, T> alternativeQuery)
        {
            CurrentController.AddAlternativeQueryToRun(alternativeQuery);
        }
    }

Having reworked a number of controller/actions to use this, I have removed a fair few abstractions I was previously using.
And the code makes more sense as a whole and in its individual pieces.

My own lesson learnt is : Don’t default all your single responsibility interactions to work through abstractions.

To wrap it up, here is what the fubuMVC action/controller looks like with this new set up:

    public class PostLoginDeciderController : ControllerWithASession
    {
        public FubuContinuation DecideWhereToGoAfterLogin(TheRequestsCurrentUser theRequestsCurrentUser)
        {
            var registrationPendingApproval = 
                Query(new NewRegistrationPendingApprovalQuery(theRequestsCurrentUser.User.Id));

            if registrationPendingApproval != null)
                return FubuContinuation.RedirectTo<PendingRegistrationController>(r => 
                                r.PendingRegistrationApproval(theRequestsCurrentUser));

            var assignmentPendingApproval = 
                Query(new AssignmentPendingApprovalQuery(theRequestsCurrentUser.User.Id));

            if (assignmentPendingApproval != null)
                return FubuContinuation.RedirectTo<PendingRegistrationController>(r => 
                                r.PendingAssignmentApproval(theRequestsCurrentUser));

            return FubuContinuation.RedirectTo<RegistrationController>(r => r.StepTwo());
        }
    }

Posted at at 7:50 AM on Thursday, March 1, 2012 by Posted by Justin Davies | 5 comments Links to this post   | Filed under: ,