/ScopedUnitOfWork

ScopedUnitOfWork is a lightweight .NET implementation of commonly used Unit of Work and Repository patterns, extended with scoped functionality to improve read performance and in-built with transactions in respect to underlying ORM tool.

Primary LanguageC#MIT LicenseMIT

ScopedUnitOfWork

Project not maintained actively, but feel free to send pull requests or use the code to inspire your own implementations

alpha / 0.1.0.0

ScopedUnitOfWork is a lightweight .NET implementation of commonly used Unit of Work and Repository patterns, extended with scoped functionality to improve read performance and in-built with transactions in respect to underlying ORM tool.

Currently supported ORMs:

  • Entity Framework Core (former Entity Framework 7)
  • Entity Framework 6

Three key features are:

  • very simple and extensible repository / uow implementation, which integrates with any IoC container out there
  • scoped (ambient) contexts
  • scoped (ambient) transactions

Simple extensible repository / uow implementation

Altough there are about a milion other repository / uow implementations out there, I never found any that was simple enough, without bloat of 100 methods, that was IoC / DI friendly and that was easily extensible (essentially respecting open / closed principle).

A pattern that worked for me for years is having a injectable factory, which creates units of work, which are then used to resolve the repositories. Something like:

using (IUnitOfWork unitOfWork = factory.Create())
{
    unitOfWork.GetRepository<ICustomerRepository>().Add(...);

    unitOfWork.Commit();
}

What I also need is that the framework uses the underlying IoC container, for example when resolving repositories like in example above. This removes any need of having concrete repository properties on UnitOfWork class which is a common anti-pattern.

Scoped contexts

Main reason for scoped / ambient contexts is usage of so called L1 cache. This is the in-built cache in the EF DbContext which is checked when retriving entities by the identifier (for example when using context.Find()). To take advantage of this caching you simply reuse the same database context for multiple operations. But, as soon as you start "sharing" these context you have to manage their lifecycles as well. There are many patterns to go about this issue, session-per-web-request or session-per-controller to just name a few. I would strongly recommend reading this excellent acricle for more info: http://mehdi.me/ambient-dbcontext-in-ef6/

How does scoped units of work look like? Like this:

using (IUnitOfWork parent = factory.Create())
{
    using (IUnitOfWork child = factory.Create())
    {
        ...
    }
}

Scoped / ambient functionality takes care that both parent and child have actually the same DbContext instance.

Scoped / ambient transactions

Very similarly to the scoped contexts, I also need to have easy to use Transaction management. Since TransactionScope is kind of no longer an option with EF6 (not recommented) / EF Core (not supported at all), I implemened the same functionality using this framework. Code says it all:

using (IUnitOfWork unitOfWork = factory.Create(ScopeType.Transactional))
{
    // everything here, as well as any other unit of work are now transactional
    using (IUnitOfWork unitOfWork = factory.Create())
    {
        ...
    }

    // commits everything
    unitOfWork.Commit();
}

Configuration and first use

The framework has IoC container usage in its blood, so you need to provide a bridge to whatever container you are using. Internally, the CommonServiceLocator is used, so is should be pretty easy.

For example, with Autofac you simple use the wrapper already defined in CommonServiceLocator project. Important part is that IServiceLocator is registered in the container itself!

// to update the Autofac container, we need to create a new ContainerBuilder and update the container with new registrations
builder = new ContainerBuilder();

// The AutofacServiceLocator is wrapper from Autofac.Extras.CommonServiceLocator
builder.Register(x => new AutofacServiceLocator(container)).As<IServiceLocator>();

builder.Update(container);

The registration above is fine as long as you have your DbContexts registered as "transient" (new instance each time when requested). However, in some cases you might have the DbContext registered per scope (like when using ASP.NET Core and Entity Framework Core). In that case, make sure you use the inner most scope as container when plugging in the CommonServiceLocator:

builder = new ContainerBuilder();

// note the dynamic resolution of ILifetimeScope
builder.Register(x => new AutofacServiceLocator(x.Resolve<ILifetimeScope>()))
    .As<IServiceLocator>();

builder.Update(container);

For other IoC containers, syntax would be different of course.

#####For a working project, please check ScopedUnitOfWork.Sample project which uses Autofac and Entity Framework 7.

After IoC integration is set up, rest should be pretty easy

  1. Your DbContext needs to be registered with your IoC container as self, since it will be internally resolved from the IoC container upon UnitOfWork creation. For example with Autofac:

    builder.RegisterType<SampleContext>().As<SampleContext>();
    

    Note: in some frameworks, like ASP.NET Core with EF Core, this will already be done with configuration code in Startup class.

  2. You need to register a factory with your concrete context. With Autofac you would do something like:

    builder.RegisterType<UnitOfWorkFactory<SampleContext>>().As<IUnitOfWorkFactory>();
    

    The context type here (SampleContext) is the exact concrete type registered as self in the step 1. Factory interface IUnitOfWorkFactory should be used for constructor injection on your objects where you want to use UoW.

  3. Register your repositories for their interfaces. For example

    builder.RegisterType<InvoiceRepository>().As<IInvoiceRepository>();
    

    This is required because you will get the repositories through uow.GetRepository() and this internally will use your IoC container.

Realistic real-world example

More complex real world usage with transactions (each unit of work represents different components which would be contained in different classes):

// our controller or some other top level service (maybe on application services layer)
// opens a spanning context - every uow underneath will have the same context
using (var spanningUnitOfWork = factory.Create())
{
    var repository = spanningUnitOfWork.GetRepository<ICustomerRepository>();
    Customer customer = repository.FindByName(firstCustomerName);

    // let's say this was a authorization check or something
    customer.Name.Should().Be(firstCustomerName);

    // now we want to call a business service which would have its own transactional unit of work in there
    using (var businessUnitOfWork = factory.Create(ScopeType.Transactional))
    {
        using (var firstDomainService = factory.Create())
        {
            firstDomainService.GetRepository<ICustomerRepository>()
                .Add(new Customer { Name = secondCustomerName });

            firstDomainService.Commit();
        }

        using (var secondDomainService = factory.Create())
        {
            secondDomainService.GetRepository<ICustomerRepository>()
                .Add(new Customer { Name = thirdCustomerName });

            secondDomainService.Commit();
        }

        // this should commit the transaction as its the top most transactional uow
        businessUnitOfWork.Commit();
    }
}

Also check the acceptance tests in the solution for actual code and examples.