Thursday, March 15, 2012

Creating a FubuMVC Behavior

As part of the day Job, our company decided to use FubuMVC for one of our monitoring and administration tools. This was all very exciting as I enjoy playing with new and shiny things. It's been a few days now and in an effort to put something back to the community I thought I'd provide you all with a few things I've learnt that might help someone out one day. I'll be demoing 3 things

Why would you want to create your own FubuMVC behaviour?

I like to think of the behaviours as a bit like the decorator pattern. They allow us to follow the Open/Closed principle in SOLID. I.e. I can add things such as

  • Logging
  • exception handling
  • unit of work
  • validation

to a code base without actually modifying the existing handler. It's one of the reasons I was so keen to try out FubuMVC. There are a few existing blog posts I've found which explain how to create a FubuMVC behaviour:

I'm going to show you how to create a transaction wrapping behaviour or unit of work behaviour, which is more simplistic that Justin Davies post

Get with the code already

My breif was to make sure that all request handlers than use an ISession are wrapped in a transaction.

First you need to create a behaviour

public class TransactionBehaviour : IActionBehavior
{
    private readonly ISession session;
    public IActionBehavior InnerBehavior { get; set; }

    public TransactionBehaviour(ISession session)
    {
        this.session = session;
    }

    public void Invoke()
    {
        using (var transaction = session.BeginTransaction())
        {
            InnerBehavior.Invoke();
            transaction.Commit();
        }
    }

    public void InvokePartial()
    {
        Invoke();
    }
}

Then you need to tell FubuMVC when and how to apply the behaviour using the following code:

public class TransactionBehaviourConfiguration : IConfigurationAction
{
    public void Configure(BehaviorGraph graph)
    {
        GenericEnumerableExtensions.Each<ActionCall>(graph.Actions()
                                    .Where(x => x.HandlerType.HasInjected<ISession>()), x => x.AddBefore(new Wrapper(typeof(TransactionBehaviour))));
    }
}

public static class TypeExtensions
{
    public static bool HasInjected<T>(this Type type)
    {
        return
            type
                .GetConstructors()
                .Any(x => x.GetParameters()
                                .Any(y => y.ParameterType.IsAssignableFrom(typeof (T))));
    }
}

Then you include the convention above in the FubuMVC Registry:

public class ConfigureFubuMVC : FubuRegistry
{
    public ConfigureFubuMVC()
    {
        IncludeDiagnostics(true);
        Routes.HomeIs<GetInputModel>();
        Views.TryToAttachWithDefaultConventions();
        ApplyHandlerConventions(typeof(HandlersMaker));
        this.UseSpark();

        ApplyConvention<TransactionBehaviourConfiguration>();  // New Line
    }
}

Lastly, as I'm injecting the ISession into my handler like this:

public class PostHandler
{
    private readonly ISession session;

    public PostHandler(ISession session)
    {
        this.session = session;
    }

    public FubuContinuation Execute(PostInputModel inputModel)
    {
        var customer = new Customer {Name = inputModel.Name};
        session.Save(customer);

        return FubuContinuation.RedirectTo(new ExampleRead.GetInputModel());
    }
}

I need to make sure that the ISession is kept for the whole of the http request using the Structure Map IoC container as follows:

public class TransactionBehaviourRegistry: Registry
{
    private const string DbName = @"c:\temp\example.db";

    public TransactionBehaviourRegistry()
    {
        ForSingletonOf<ISessionFactory>().Use(CreateSessionFactory);
        // Keep ISession for whole of Http Request
        For<ISession>().HttpContextScoped().Use(x => x.GetInstance<ISessionFactory>().OpenSession());
    }

    private ISessionFactory CreateSessionFactory()
    {
        return Fluently.Configure()
                .Database(
                    SQLiteConfiguration.Standard
                        .UsingFile(DbName)
                )
                .Mappings(m => m.FluentMappings.AddFromAssemblyOf<CustomerMap>())
                .ExposeConfiguration(BuildSchema)
                .BuildSessionFactory();
    }

    private void BuildSchema(Configuration config)
    {
        // Only create once
        if (File.Exists(DbName))
            return;
        new SchemaExport(config)
            .Create(false, true);
    }
}

Now if I navigate to _fubu I can see that the handlers which have an ISession injected are now wrapped in a transaction.

Any handlers which don’t make use of ISession are not wrapped in a transaction.

Conclusion

The code for this example can be found on my git hub account here. As you can see with very little code you can add code to all the handlers which use an ISession. Good luck and let me know if I’ve done anything wrong or if you can think of any improvements. Would be great to hear your feedback.

3 comments:

  1. Hey David,

    Great post! Two quick things to note:

    1)So long as you register your dependencies transiently, the Nested Container in Fubu will do the "http request scoped" business for you.

    2) I see that you're using Advanced Diagnostics. Did you know that it also records requests? The visualizers for a request aide in understanding how the behaviors all fit together.

    Always great to see more Fubu users :)

    ReplyDelete
  2. Thanks Josh... especially for the feedback. Great to be on board. I'm really enjoying FubuMVC.. think I've only scratched the surface so far.

    ReplyDelete
  3. This may or may not apply in your situation.

    For most of our applications our transaction behavior for the InvokePartial call will just call invoke partial for the next behavior. You'll get surprising results in some cases if you don't continue with the partial path. Also in many cases on the partial path you can assume that there is already a transaction present and not need to start a second.

    Good stuff, keep 'em coming!

    ReplyDelete