Wednesday, March 7, 2012

Unit of work using lambdas

Are you tired of writing into log file on every begin and end of the method ? Use AOP. Well, not yet ready for post-compiler magic or dynamic runtime proxies ? Let's try lambdas (again).

static void Main(string[] args)
{
    Scope<LoggingFrame>.Wrap(() =>
    {
        Console.WriteLine("Hello world");
    });
}

Would produce this console
2012-03-07 00:46:58,248 DEBUG - BEGIN Main
Hello world
2012-03-07 00:46:58,258 DEBUG - END   Main

How to implement that ?
[DebuggerStepThrough]
public class Scope<TAdvice> where TAdvice : IAdvice, new()
{
    public static void Wrap(Action body)
    {
        IAdvice advice = Activator.CreateInstance<TAdvice>();
        advice.OnEntry(body);
        try
        {
            body();
            advice.OnLeave(body);
        }
        catch (Exception ex)
        {
            advice.OnException(body, ex);
            throw;
        }
        finally
        {
            advice.OnFinally(body);
        }
    }
}
public interface IAdvice
{
    void OnEntry(Delegate body);
    void OnLeave(Delegate body);
    void OnException(Delegate body, Exception exception);
    void OnFinally(Delegate body);
}
Implementation of the LoggingFrame advice is trivial.

Note that you could get similar behavior with IDisposable and using keyword, but you would not be able to log pending exception.
You could also think about TransactionScope, which would call tx.Complete() automatically when no exception is thrown.
Further improvement is to use dependency injection to instantiate the advises.

Another use-case is to implement Unit of Work or session/call context, while using TSL to reach topmost frame. I used it for NHibernate Session and EF DbContext (unit of work) recently. Interesting related article here.

private void Main(string[] args)
{
    Scope<UnitOfWork>.Wrap(session =>
    {
        var people = session.Person
            .Where(person => person.FirstName == "Pavel")
            .ToList();

        NestedLogic(people);

        //DbContext.SaveChanges() will be called here
    });

}

private void NestedLogic(IList<Person> people)
{
    //this will lookup parent session in TSL and reuse it
    Scope<UnitOfWork>.Wrap(session =>
    {
        foreach (var person in people)
        {
            if (person.LastName == "Savara")
            {
                person.Coder = true;
            }
            else
            {
                session.Person.Remove(person);
            }
        }
    });
}

Composition of scopes could be beautified.
Scope<LoggingFrame, TransactionFrame>.Wrap(() =>
{
    throw new Exception("rollback please");
});


[DebuggerStepThrough]
public class Scope<TOuterAdvice, TInnerAdvice> 
  where TOuterAdvice : IAdvice, new() 
  where TInnerAdvice : IAdvice, new()
{
    public static void Wrap(Action body)
    {
        Scope<TOuterAdvice>.Wrap(()=> Scope<TInnerAdvice>.Wrap(body));
    }
}

All code in the article is simplified and real implementation is exercise for readers.

Enjoy :-)

No comments: