Introduction
If you have more than a few years of experience within domain-driven design (DDD), most certainly, you have recognized some kind of overall pattern in the type of problems you have to solve – regardless of the type of applications you are working on. I certainly know that I have.
No matter whether you develop desktop applications, web applications or web APIs, you will almost always find yourself in a situation where you have to establish a mechanism for creating, persisting and maintaining state of various entities in the application domain model. So, every time you start up a new project, you have to do a lot of yak shaving to establish this persistence mechanism, when what you really want to do is to work on establishing the domain model – the actual business functionality of your application.
After several iterations through various projects, I have established a practice that works for me in almost any situation. This practice allows you to abstract the entity persistence (the yak shaving…) so that you can easily isolate the nitty-gritty implementation details of this and focus on developing your genuine business functionality. Eventually, of course you have to deal with the implementation of the persistence layer, but the value of being able to develop – and not at least test – your domain model in isolation without having to care about the persistence details is tremendous. Then, you can start out with developing and testing your domain model against fake repositories. Whether you eventually end up making simple file based repositories or decide to go full-blown RDBMS doesn't matter at this point in time.
For this article, I have digested this practice of mine into something I call a Domain Services Library. This library is super lightweight comprising only a few plain vanilla C# classes. The library itself has no 3rd party dependencies whatsoever. No ORM is involved – the repositories can be anything from in-memory objects to RDBMS.
Background
The technical ingredients of this practice – or library if you wish – are nothing else than some general object oriented software development principles know as SOLID, the repository pattern and, in particular, dependency injection.
There are numerous resources out there describing these principles and patterns. I have made my own attempt in a series of blog posts that can be found here.
The Domain Services Library Code
Let's dig into the matter. The overall idea is to create well-defined points of interaction – so called seams - in the form of repository interfaces between the domain model services and the persistence layer repositories. Here exemplified by the dependency graph of a product service and its corresponding repository:
The abstraction of a repository is defined in the IRepository
and IReadOnlyRepository
interfaces.
public interface IRepository<TEntity, in TId> :
IReadOnlyRepository<TEntity, TId> where TEntity : IEntity<TId>
{
void Add(TEntity entity);
void Remove(TId id);
void Update(TEntity entity);
}
Notice that IRepository
inherits the signature of IReadOnlyRepository
. In other words, IRepository
is an extension of IReadOnlyRepository
.
public interface IReadOnlyRepository<TEntity, in TId> where TEntity : IEntity<TId>
{
int Count { get; }
bool Contains(TId id);
E Get(T id);
IQueryable<TEntity> Get(Expression<Func<TEntity, bool>> predicate);
IEnumerable<TEntity> GetAll();
}
Obviously, the repository abstraction could have been kept in a single interface, but due to the Interface segregation principle, it makes sense to split it into (at least) two separate interface. You might end up having to implement a simple read-only repository and consumers should not be forced to throw NotImplementedException
s for unsupported features. In real production code, you might consider splitting the repository interface into even more sub-interfaces.
The abstraction of an entity in the domain model is defined by the generic IEntity
interface.
public interface IEntity<TId> : INotifyPropertyChanged, INotifyPropertyChanging
{
TId Id { get; set; }
string Name { get; set; }
}
The generic type parameter TId
represents the type of the ID of the entity which typically varies depending on the type of entity. You might prefer string IDs for some entities while other types of entities might require for example integer or GUID IDs. Also notice that IEntity
inherits the INotifyPropertyChanged
and INotifyPropertyChanging
interfaces. This is to support possibly data binding which comes in very handy during UI development.
The entity abstraction is brought one step further by the generic abstract
BaseEntity
class which provides a basic implementation of the IEntity
interface.
public abstract class BaseEntity<TId> : IEntity<TId>
{
private TId _id;
private string _name;
protected BaseEntity(TId id, string name)
{
_id = id;
_name = name;
}
...
}
The domain service abstractions are provided by two generic abstract
base classes called BaseService
and BaseReadOnlyService
using a repository and a read-only repository respectively. Not surprisingly, the BaseService
extends the BaseReadOnlyService
. The repository dependency is handled by injecting a repository instance through the constructor. The service does not need to know anything about the concrete implementation of the repository other than the fact that it fulfils the contract as defined by the IRepository
interface. This is dependency injection in action.
public abstract class BaseService<TEntity, TId> : BaseReadOnlyService<TEntity, TId>
where TEntity : IEntity<TId>
{
private readonly IRepository<TEntity, TId> _repository;
protected BaseService(IRepository<TEntity, TId> repository)
: base(repository)
{
_repository = repository;
}
...
}
The BaseService
raises Adding
and Added
events when entities are added. The Adding
event supports canceling. Similar events are raised when entities are updated or removed.
All of the abstract
base classes provide default implementations of some of the interface members. This has the advantage, that you can establish a functional implementation of these abstractions very quickly by implementing only the mandatory abstract
members. The default implementations in the base classes will typically be marked virtual
so you can override
them in your own implementation, if you can come up with a better implementation yourself.
To support unit testing, a generic FakeRepository
is provided. In here, "persistence” is done in an in-memory object – a Dictionary
object. This is obviously useless in a real application but perfect as a fake repository for unit testing.
public class FakeRepository<TEntity, TId> : IRepository<TEntity, TId> where TEntity : IEntity<TId>
{
public FakeRepository()
{
Entities = new Dictionary<TId, TEntity>();
}
protected Dictionary<TId, TEntity> Entities { get; }
...
}
Using the Domain Services Library
Now, let's look at an example of usage of the domain services library. Let's add support for product management. First, you will create a Product
class by deriving from the generic abstract
BaseEntity
class. The generic type parameter defining the type of the Id
-property is set to Guid
. In addition to the Id
- and Name
property defined in IEntity
, it seems reasonable to add at least a Price
property to the Product
class. For simplicity, the data binding support is ignored for the Price
property:
public class Product : BaseEntity<Guid>
{
public Product(string name)
: base(Guid.NewGuid(), name)
{
}
public decimal Price { get; set; }
}
Now, let's say that you want to extend the product repository with a method to detect whether the repository already contains a product with the same name. Then, you can just create an IProductRepository
interface by extending the IRepository
interface:
public interface IProductRepository : IRepository<Product, Guid>
{
bool ContainsName(string name);
}
Finally, it is time to create the product service itself. This is basically done by deriving from the generic abstract
BaseService
class. Since all the methods in the BaseService
class are declared as virtual
, you can decide to override them - for example to add further constraints when adding a product. The BaseService
already throws an exception if you try to add an entity with an already existing ID, but in the below implementation, exceptions are also thrown if a product with the given name already exists or if the product name is empty or undefined.
public class Products : BaseService<Product, Guid>
{
private readonly IProductRepository _repository;
public Products(IProductRepository _repository)
: base(repository)
{
_repository = repository;
}
public override void Add(Product product)
{
if (_repository.ContainsName(product.Name))
{
throw new ArgumentException
($"There is already a product with the name '{product.Name}'.", nameof(product));
}
if (string.IsNullOrEmpty(product.Name))
{
throw new ArgumentException
("Product name cannot be null or empty string.", nameof(product));
}
base.Add(product);
}
}
In real production code, you would probably now extend the product service with additional functionality – for example for calculating discounted prices, currency management, etc.
Needless to say, the pattern for establishing similar services for other entities such as for example users, customers, campaigns, etc. is exactly the same.
Now, let's write some test code. Because you made the IProductRepository
as an extension of the IRepository
interface, first you need to create a FakeProductRepository
class as an extension of the FakeRepository
class. The ContainsName()
method must be implemented – at least if you want to test functionality depending on it.
internal class FakeProductRepository : FakeRepository<Product, Guid>, IProductRepository
{
public bool ContainsName(string name)
{
return Entities.Values.Any(p => p.Name.Equals(name));
}
}
Now, you can test for example that the expected exception is thrown if trying to add a product with an already existing name. Notice how the FakeProductRepository
is injected into the Products
service using constructor injection:
[Fact]
public void AddWithExistingNameThrows()
{
var products = new Products(new FakeProductRepository());
var product = new Product("MyProduct name");
products.Add(product);
var productWithSameName = new Product(product.Name);
Assert.Throws<ArgumentException>(() => products.Add(productWithSameName));
}
And here is a test verifying that the Deleting
and Deleted
events are properly raised when deleting a product:
[Fact]
public void EventsAreRaisedOnRemove()
{
var raisedEvents = new List<string>();
var products = new Products(new FakeProductRepository());
products.Deleting += (s, e) => { raisedEvents.Add("Deleting"); };
products.Deleted += (s, e) => { raisedEvents.Add("Deleted"); };
var product = new Product("MyProduct name");
products.Add(product);
products.Remove(product.Id);
Assert.Equal("Deleting", raisedEvents[0]);
Assert.Equal("Deleted", raisedEvents[1]);
}
Finally, here is a test proving that the query mechanism using lambda expressions works as intended:
[Fact]
public void GetQueryableIsOk()
{
var products = new Products(new FakeProductRepository());
var coke = new Product("Coke") {Price = 9.95M};
var cokeLight = new Product("Coke Light") {Price = 10.95M};
var fanta = new Product("Fanta") {Price = 8.95M};
products.Add(coke);
products.Add(cokeLight);
products.Add(fanta);
var cheapest = products.Get(p => p.Price < 10M).ToList();
var cokes = products.Get(p => p.Name.Contains("Coke")).ToList();
Assert.Equal(2, cheapest.Count());
Assert.Equal(2, cokes.Count());
}
Many more tests are available in the sample code.
The Components
The source code is divided into the following DLLs (projects):
The DomainServices
project contains the basic abstractions in the form of generic interfaces and abstract
classes – for example, an abstract
BaseService
class and a generic IRepository
interface. This is the Domain Services library itself.
The MyServices
project contains some concrete implementations and extensions of the DomainServices
abstractions – for example, a Products
class and an IProductRepository
interface.
The MyServices.Test
project contains the unit test classes for the MyServices
types.
The MyServices.Data
project contains concrete implementations of the repository interfaces defined in MyServices
– for example, a JsonProductRepository
, which is an implementation of IProductRepository
that stores products, serialized in a JSON file.
Summary
This article provides a concrete example of a very simple and lightweight – yet useful – domain services library. The ingredients are some general object oriented software development principles as well as the repository pattern and dependency injection. The library consists of plain vanilla C# classes only.
The loose coupling achieved by the use of dependency injection makes it very easy to establish unit tests of domain functionality using fake repository objects. A generic FakeRepository
class is provided for this purpose.
The overall design principles are described in more detail in another CodeProject article of mine.
Practical Information
The tests are written using xUnit.NET which is a personal favourite of mine. xUnit.NET is available through the NuGet Package Manager in Visual Studio. In production code, you should seriously consider using supplementary unit test frameworks such as Moq and Autofixture to help you streamline mocking and fixture setup. Both libraries are available through NuGet. I have written another CodeProject article about this.
Production code would obviously also require concrete implementation of the various repositories. In the sample code, I added a simple JsonProductRepository
for product persistence in a JSON file. This repository uses the Json.NET library which is available through NuGet. I did not provide unit tests for this repository.
As the domain services library is leveraging dependency injection, it is ideal for usage with a dependency injection container such as for example NInject or Unity – both available through NuGet.
The sample code is made in Visual Studio 2017 (C# 7) using .NET Framework 4.6.1.
History
- 23rd February, 2019
- Refactored code to use:
- auto properties
- inline out variables
- expression-bodied members
nameof
operator null
propagation operator string
interpolation
- Updated to VS 2017 (C# 7) and .NET Framework 4.6.1
- Updated to xUnit.NET 2.4.1 and Json.NET 12.0.1
- Migrated from NuGet
packages.config
to PackageReference
- Introduced a new Components chapter
- 8th July, 2015
- Introduced a more standardized coding style - including more descriptive generic type parameters
- Updated unit tests to use most recent xUnit.NET version (2.0.0)