The Repository Pattern

Introduction

When accessing data from a data source we have several well documented possibilities. Martin Fowler e.g. describes several of them in his PoEAA book.

  • Table data gateway
  • Row data gateway
  • Active record
  • Data mapper

When applying DDD (domain driven design) we often use the so called Repository Pattern to access the data needed by the domain model.

What is a Repository

Martin Fowler writes:

"A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction. Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes. Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer. Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers."

Implementation

In DDD we have the notion of aggregates. An aggregate is "A cluster of associated objects that are treated as a unit for the purpose of data changes. External references are restricted to one member of the Aggregate, designated as the root. A set of consistency rules applies within the Aggregate's boundaries.".

Usually one defines a repository per aggregate in the domain. That is: we don't have a repository per entity! If we have a look at a simple order entry system the entity Order might be the root of a Order aggregate. Thus we will have an Order Repository.

The interface (or contract) of the repository is considered as a part of the domain model where as the implementation of the repository is infrastructure specific and thus does NOT belong to the domain model.

When dealing with aggregates most of the time we need exactly 3 persistence related operations

  • get an aggregate by it's id (primary key)
  • add a new aggregate to the repository
  • remove an aggregate from the repository

Depending on the context there might be other persistence related operations that are needed but the most common and often used ones are the three mentioned above. This leads us to the following minimal contract (interface) with the repository

public interface IOrderRepository
{
    Order GetById(int id);
    void Add(Order order);
    void Remove(Order order);
}

In the above sample we assume that the primary key of the Order is a surrogate key and is of type int.

Now let's assume that we have identified a second aggregate in our domain model, the Product aggregate. Then we can immediately deduce the appropriate repository interface from the interface to the order repository. We have

public interface IProductRepository
{
    Product GetById(int id);
    void Add(Product product);
    void Remove(Product product);
}

Now this looks very similar to the first interface and we can generalize the interface by using a generic parameter. We then have

public interface IRepository<T>
{
    T GetById(int id);
    void Add(T entity);
    void Remove(T entity);
}

Now the interfaces to the product and the order repository will just be

public interface IOrderRepository : IRepository<Order> { }
public interface IProductRepository : IRepository<Product> { }

A fake repository

For testing purposes we can implement a fake repository for the product aggregate. In this case it will be a dictionary with some predefined products. The key to each product will be its primary key. In our simple sample the Product aggregate consists only of the Product entity

public class Product
{
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }
    public virtual int ReorderLevel { get; set; }
    public virtual bool Discontinued { get; set; }
}

This is a very simple entity but it is sufficient for our purposes. Now let's have a look at the implementation of the faked repository

public class ProductRepositoryFake : IProductRepository
{
    private readonly Dictionary<int, Product> dictionary;
 
    public ProductRepositoryFake()
    {
        dictionary = new Dictionary<int, Product>();
        dictionary.Add(1, new Product {Id = 1, Name = "Product 1", ReorderLevel = 10, Discontinued = false});
        dictionary.Add(2, new Product {Id = 2, Name = "Product 2", ReorderLevel = 15, Discontinued = false});
        dictionary.Add(3, new Product {Id = 3, Name = "Product 3", ReorderLevel = 10, Discontinued = false});
        dictionary.Add(4, new Product {Id = 4, Name = "Product 4", ReorderLevel = 12, Discontinued = false});
        dictionary.Add(5, new Product {Id = 5, Name = "Product 5", ReorderLevel = 20, Discontinued = true});
    }
 
    public Product GetById(int id)
    {
        return dictionary[id];
    }
 
    public void Add(Product product)
    {
        dictionary.Add(product.Id, product);
    }
 
    public void Remove(Product product)
    {
        dictionary.Remove(product.Id);
    }
}

In the constructor I instantiate a dictionary which takes as key an int (the type of the primary key in our case) and as value a Product instance. Then I define several sample Product instances which I fill into the dictionary. The implementation of the three methods GetById, Add and Remove is straight forward and needs no further explanation.

Now let's write some unit tests where we use this implementation of the repository. Note that the special methods (e.g. ShouldEqual, ShouldNotBeNull, ShouldBeThrownBy, etc.) found in the code below are extension methods and can be found in the code accompanying this post. They aid to make the test code more readable than when using the Assert.Equals(...), etc. syntax.

[TestFixture]
public class FakeRepositoryTester
{
    private IProductRepository repository;
 
    [SetUp]
    public void SetupContext()
    {
        repository = new ProductRepositoryFake();
    }
 
    [Test]
    public void can_load_a_product_by_its_id_from_the_repository()
    {
        var product = repository.GetById(2);
 
        product.ShouldNotBeNull();
        product.Id.ShouldEqual(2);
    }
 
    [Test]
    public void can_add_a_new_product_to_the_repository()
    {
        repository.Add(new Product {Id = 99, Name = "Product 99", ReorderLevel = 13, Discontinued = false});
        
        // let's try to load this new product
        var product = repository.GetById(99);
        product.Id.ShouldEqual(99);
        product.Name.ShouldEqual("Product 99");
    }
 
    [Test]
    public void can_remove_an_existing_product_from_the_repository()
    {
        var product = repository.GetById(1);
        repository.Remove(product);
 
        typeof (KeyNotFoundException).ShouldBeThrownBy(() => repository.GetById(1));
    }
}

In the SetupContext method (which will be executed before each test) I define the repository to be an instance of type ProductRepositoryFake. Note that the repository field is declared as type of IProductRepository!

The first test method verifies that the GetById method works as expected. Which indeed it does since when running the test it is green. The second test verifies that the Add method works. First I add a new (non existing) product instance. Then I immediately try to reload this new instance by its id. Then the reloaded instance is tested whether it is the one expected (by comparing some of its properties).

Last but not least the third method tests the Remove method. In the first line I load a product instance from the repository which I know exists. This instance is then removed from the repository. In the third line I test whether the product was really removed from the repository by expecting an exception when trying to get the (previously removed) product from the repository again.

Please note that in the above test code there is only one place where the specific implementation of the repository is of interest. It's the line in the SetupContext method where the repository instance is created. The rest of the code is fully agnostic regarding the specific implementation of the repository. That's an important fact to remember. That means that I can exchange the implementation used in the above code by only changing a single line of code!

An implementation for NHibernate

Now let's try a first implementation for the repository that uses NHibernate. First I need a session provider. I use the following implementation

public class SessionProvider
{
    private static Configuration configuration;
    private static ISessionFactory sessionFactory;
 
    public static Configuration Configuration
    {
        get
        {
            if(configuration == null)
            {
                configuration = new Configuration();
                configuration.Configure();                              // A
                configuration.AddAssembly(typeof (Product).Assembly);   // B
            }
            return configuration;
        }
    }
 
    public static ISessionFactory SessionFactory
    {
        get
        {
            if (sessionFactory == null)
                sessionFactory = Configuration.BuildSessionFactory();
            return sessionFactory;
        }
    }
 
    private SessionProvider()
    { }
 
    public static ISession GetSession()
    {
        return SessionFactory.OpenSession();
    }
}

In the above sample code the session factory is only built on demand and only once during the life-time of the application (the construction of the session factory is an expensive operation, on the other hand the session factory is thread safe). I make the following assumptions

  • the existence of a hibernate.cfg.xml for configuration of the connection properties (A)
  • all mapping files for the domain entities can be found in the assembly where the Product entity is defined (B)

With the aid of this SessionProvider class the implementation of the repository for e.g. the Product entity is really trivial

public class ProductRepositoryNH : IProductRepository
{
    private static ISession GetSession() { return SessionProvider.GetSession(); }
 
    public Product GetById(int id)
    {
        using (var session = GetSession())
            return session.Get<Product>(id);
    }
 
    public void Add(Product product)
    {
        using (var session = GetSession())
            session.Save(product);
    }
 
    public void Remove(Product product)
    {
        using (var session = GetSession())
            session.Delete(product);
    }
}

We have a private helper function GetSession which just returns a new session object whenever accessed. The other methods use this helper property for their operations. Note the usage of the using statement to guarantee that the session object is released whenever the database operation is finished.

But the above implementation (although working) has an important flaw and thus can hardly be used in a real application! It's the fact that the repository uses a different session instance for each operation. In a real application often several operations have to be executed inside a single transaction and thus inside a single session (the reason is that a transaction cannot span multiple sessions - at least when not using distributed transactions).

The Unit of Work to the rescue

We can resolve the above dilemma by applying the Unit of Work (UoW) pattern (for a profound discussion of this patterns please refer to this article)! The UoW manages a session for us. I can then refactor my implementation of the product repository as follows

public class ProductRepository : IProductRepository
{
    private ISession Session { get { return UnitOfWork.CurrentSession; } }
 
    public Product GetById(int id)
    {
        return Session.Get<Product>(id);
    }
 
    public void Add(Product product)
    {
        Session.Save(product);
    }
 
    public void Remove(Product product)
    {
        Session.Delete(product);
    }
}

Unit Tests

First I implement a base class for unit testing repositories. This base class configures my unit of work and re-creates the schema for each test (I'm using SqLite in in-memory mode as my database)

public class RepositoryFixtureBase<T>
{
    [TestFixtureSetUp]
    public void TestFixtureSetup()
    {
        UnitOfWork.Configuration.AddAssembly(typeof(T).Assembly);
    }
 
    [SetUp]
    public void SetupContext()
    {
        UnitOfWork.Start();
 
        new SchemaExport(UnitOfWork.Configuration)
            .Execute(false, true, false, false, UnitOfWork.CurrentSession.Connection, null);
 
        Context();
    }
 
    [TearDown]
    public void TearDownContext()
    {
        UnitOfWork.Current.TransactionalFlush();
        UnitOfWork.Current.Dispose();
    }
 
    protected virtual void Context() { }
}

with the aid of the above base class the unit tests for my product repository look like this

[TestFixture]
public class ProductRepository_Tester : RepositoryFixtureBase<Product>
{
    private IProductRepository repository;
    private Product[] products;
 
    protected override void Context()
    {
        products = new[]
                       {
                           new Product {Id = 1, Name = "Product 1", ReorderLevel = 10, Discontinued = false},
                           new Product {Id = 2, Name = "Product 2", ReorderLevel = 15, Discontinued = false},
                           new Product {Id = 3, Name = "Product 3", ReorderLevel = 10, Discontinued = false},
                           new Product {Id = 4, Name = "Product 4", ReorderLevel = 12, Discontinued = false},
                           new Product {Id = 5, Name = "Product 5", ReorderLevel = 20, Discontinued = true},
                       };
        
        foreach (var product in products)
            UnitOfWork.CurrentSession.Save(product);
 
        UnitOfWork.CurrentSession.Flush();
        UnitOfWork.CurrentSession.Clear();
 
        repository = new ProductRepository();
    }
 
    [Test]
    public void can_load_product()
    {
        var p = repository.GetById(2);
        p.ShouldNotBeNull();
        p.Id.ShouldEqual(products[1].Id);
    }
 
    [Test]
    public void can_add_a_product_to_the_repository()
    {
        repository.Add(new Product { Id = 6, Name = "Product 6", ReorderLevel = 6, Discontinued = false });
 
        UnitOfWork.CurrentSession.Flush();
        UnitOfWork.CurrentSession.Clear();
        var prod = UnitOfWork.CurrentSession.Get<Product>(6);
        prod.ShouldNotBeNull();
    }
 
    [Test]
    public void can_remove_product_from_repository()
    {
        repository.Remove(products[2]);
 
        UnitOfWork.CurrentSession.Flush();
        UnitOfWork.CurrentSession.Clear();
        var prod = UnitOfWork.CurrentSession.Get<Product>(products[2].Id);
        prod.ShouldBeNull();
    }
}

In the Context method (which is executed before each test) I set up my boundary conditions (=the context). I define an array of products which are then inserted into the database. After inserting these products the current session is flushed and then cleared.

In the first test method can_load_product I try to load a product with the id equal to 2. This product exists in the database since I have applied the boundary conditions. After loading the product I check whether it has really been loaded (ShouldNotBeNull) and that I indeed received the product with the requested id.

In the second test method can_add_a_product_to_the_repository I first try to add a new product instance to the repository. I then flush the current session and clear it. Then I try to re-load the new product from the repository and verify that it is not null. Note that if I wouldn't clear the session after inserting the product, the product would still be in the first level cache and NHibernate would not query the database when I execute the ...Get<Product>(id) method but rather return the object in the cache.

In the third and last test method can_remove_product_from_repository test I try to remove an existing product from the repository. Now when trying to reload the deleted product I should get null.

A generic base repository

When having a look again at our product repository (and possibly at the order repository whose implementation I leave as an exercise for you) we can see some room left for improvement. What if I implement a base repository which in a generic way implements all the common functionality. This leads to the following implementation

public class Repository<T> : IRepository<T>
{
    public ISession Session { get { return UnitOfWork.CurrentSession; } }
 
    public T GetById(int id)
    {
        return Session.Get<T>(id);
    }
 
    public ICollection<T> FindAll()
    {
        return Session.CreateCriteria(typeof(T)).List<T>();
    }
 
    public void Add(T product)
    {
        Session.Save(product);
    }
 
    public void Remove(T product)
    {
        Session.Delete(product);
    }
}

I have taken the code from the ProductRepository class and just replaced any occurrence of Product with the generic parameter T.

Note that this base class can now easily be extended to contain more functionality which is used across repositories for different entities. As an example I have added a FindAll method in the above implementation which just returns a list of all items of the specific type.

Do we still need the ProductRepository and/or OrderRepository classes? Well, it depends. If our application has no other needs regarding the product and order repository then no, we can just use the generic base repository. On the other hand if we want to provide a specific query for the product aggregate (which in turn makes no sense for the order aggregate) we explicitly define a product repository which is a child of the generic base repository.

Let's take this as an example

public interface IProductRepository : IRepository<Product>
{
    ICollection<Product> FindAllDiscontinuedProducts();
}

Here I have declared the need for a query that returns the list of discontinued products (note that there is no notion of discontinued orders...). The implementation is straight forward

public class ProductRepository : Repository<Product>, IProductRepository
{
    public ICollection<Product> FindAllDiscontinuedProducts()
    {
        return Session.CreateCriteria(typeof (Product))
            .Add(Restrictions.Eq("Discontinued", true))
            .List<Product>();
    }
}

The above method hides the infrastructure related details from my domain and/or application and provides a business friendly interface

Let's have a look how I can test the above method

public void can_load_all_discontinued_products()
{
    var discontinuedProducts = repository.FindAllDiscontinuedProducts();
 
    discontinuedProducts.Count.ShouldBeGreaterThan(0);
    discontinuedProducts.ToList().ForEach(p=>p.Discontinued.ShouldBeTrue());
}

I expect the method to return at least 1 product and each returned product should have its property Discontinued set to true.

Code

The code to this post can be found here.

Summary

I have discussed the repository pattern which is used to retrieve data from external data sources (e.g. databases) in an easy and consistent way. Its main benefit is a clean separation of the domain and the data mapping layer. I have presented an implementation of the pattern for NHibernate. This implementation makes use of the Unit of Work which is another well known pattern which I have introduced in a previous post.

Enjoy

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值