Ambient Context with Dapper

I think anyone who's ever written data access code has at some point explored the Unit-of-Work (UOW) pattern. This pattern presets a great way to manage connections and transactions, which are arguably some of the most important concepts in an application that uses a database.

However, the reason why I've never really liked using UOW is because more often than not you end up with a God-object that is your UOW which knows everything about all of the repositories and entities in the project. I think there is a better way.

Meet the Ambient Context pattern, which is actually a commonly occurring concept throughout .NET framework (ie System.Web.HttpContext, System.ActivationContext and etc) that's been significantly less studied in comparison to other design patterns.

Ambient Context allows one to setup a context, typically at an entry-point of an operation or a request, which becomes available to the rest of the system via a static property or method. This is a great pattern to manage cross-cutting concerns in your code. So why not use it for connection and transaction management, I thought? Well, say hi to Dapper.AmbientContext which allows you to do just that.

Let's see how it works. First thing you'll need is to configure a database connection factory for whatever database engine you're using. Here's one for SQL Server.

public class SqlServerConnectionFactory : IDbConnectionFactory  
{
    private readonly string _connectionString;

    public SqlServerConnectionFactory(string connectionString)
    {
        _connectionString = connectionString;
    }

    public IDbConnection Create()
    {
        return new SqlConnection(_connectionString);
    }
}

Then configure the default ambient context storage. This needs to be done somewhere at the start of your application.

// For .NET Classic
AmbientDbContextStorageProvider.SetStorage(new LogicalCallContextStorage());

// For .NET Core
AmbientDbContextStorageProvider.SetStorage(new AsyncLocalContextStorage());  

Finally, wire up the 3 dependencies using your favorite dependency injection container. The example below is using SimpleInjector.

Container container = new Container();

container.RegisterSingleton<IDbConnectionFactory>(() => new SqlServerConnectionFactory("your connection string"));  
container.RegisterSingleton<IAmbientDbContextFactory, AmbientDbContextFactory>();  
container.Register<IAmbientDbContextLocator, AmbientDbContextLocator>();  

The ambient context is now ready for use. Now let's see how we can consume it. There is no one way to do this, but here is an example just to give you an idea. We'll create an abstract repository class which has a dependency on IAmbientDbContextLocator. As the name suggests, this is the thing that will locate an active ambient context, so it can be consumed by the derived repository class.

public abstract class AbstractRepository  
{
    private readonly IAmbientDbContextLocator _ambientDbContextLocator;

    protected AbstractRepository(IAmbientDbContextLocator ambientDbContextLocator)
    {
        _ambientDbContextLocator = ambientDbContextLocator;
    }

    protected IAmbientDbContextQueryProxy Context
    {
        get { return _ambientDbContextLocator.Get(); }
    }
}

And the derived class...

public class PostRepository : AbstractRepository, IPostRepository  
{
    public PostRepository(IAmbientDbContextLocator ambientDbContextLocator) : base(ambientDbContextLocator)
    {
    }

    public void Delete(int postId)
    {
        Context.Execute("DELETE FROM Posts WHERE PostId = @PostId;", new { PostId = postId });
    }
}

Notice, what you're actually consuming is a proxy object for Dapper. You're not opening connections or starting transactions here. That will happen in your service!

public class PublisherService  
{
  private readonly IPostRepository _postRepository;
  private readonly IAmbientDbContextFactory _ambientDbContextFactory;

  public PublisherService(IAmbientDbContextFactory ambientDbContextFactory, IPostRepository postRepository)
  {
     _ambientDbContextFactory = ambientDbContextFactory;
     _postRepository = postRepository;
  }

  public void DoSomeWork()
  {
    using (var context = _ambientDbContextFactory.Create())
    {
      _postRepository.Delete(1);

      context.Commit();
    }
  }
}

The service uses an instance of IAmbientDbContextFactory which will begin new context with new connection and transaction (optionally) via the Create method.

While the example service above only needs the IPostRepository instance, you can easily see that any number of data access components can be consumed here. And they will all share the same connection and transaction from ambient context. To read more about the capabilities of this tiny library, check out the Github wiki.

In the next post we'll talk about implementing resiliency and fault handling for database connections and commands using Polly.

Happy coding ;)