Click here to Skip to main content
15,883,883 members
Articles / Programming Languages / C#

Lightweight Domain Services Library

Rate me:
Please Sign up or sign in to vote.
4.94/5 (55 votes)
23 Feb 2019CPOL9 min read 84K   1.5K   119   15
This article provides a concrete example of a very simple and lightweight – yet useful – domain services library.

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:

Image 1

The abstraction of a repository is defined in the IRepository and IReadOnlyRepository interfaces.

C#
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.

C#
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 NotImplementedExceptions 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.

C#
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.

C#
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.

C#
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.

C#
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:

C#
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:

C#
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.

C#
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.

C#
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:

C#
[Fact]
public void AddWithExistingNameThrows()
{
    // Setup fixture
    var products = new Products(new FakeProductRepository());
    var product = new Product("MyProduct name");
    products.Add(product);
    var productWithSameName = new Product(product.Name);
 
    // Exercise system and verify outcome
    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:

C#
[Fact]
public void EventsAreRaisedOnRemove()
{
    // Setup fixture
    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);
 
    // Exercise system
    products.Remove(product.Id);
 
    // Verify outcome
    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:

C#
[Fact]
public void GetQueryableIsOk()
{
    // Setup fixture
    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);
 
    // Exercise system
    var cheapest = products.Get(p => p.Price < 10M).ToList();
    var cokes = products.Get(p => p.Name.Contains("Coke")).ToList();
 
    // Verify outcome
    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):

Image 2

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)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Denmark Denmark
I am a software architect/developer/programmer.

I have a rather pragmatic approach towards programming, but I have realized that it takes a lot of discipline to be agile. I try to practice good craftsmanship and making it work.

Comments and Discussions

 
SuggestionMore advanced example? Pin
Lechuss1-Mar-19 5:42
Lechuss1-Mar-19 5:42 
QuestionRelated entities Pin
Member 1416375326-Feb-19 8:37
Member 1416375326-Feb-19 8:37 
AnswerRe: Related entities Pin
L. Michael28-Feb-19 21:21
L. Michael28-Feb-19 21:21 
QuestionNice article but .... Pin
Rahman Mahmoodi28-Feb-14 13:14
Rahman Mahmoodi28-Feb-14 13:14 
AnswerRe: Nice article but .... Pin
L. Michael1-Mar-14 6:43
L. Michael1-Mar-14 6:43 
AnswerRe: Nice article but .... Pin
L. Michael3-Mar-14 9:59
L. Michael3-Mar-14 9:59 
Hi Rahman,

As suggested by you, I discarded the IProduct (and IUser) interfaces. I modified the article and uploaded a new set of files.

Regards
Lars
GeneralRe: Nice article but .... Pin
krepak3-Mar-14 10:57
krepak3-Mar-14 10:57 
GeneralRe: Nice article but .... Pin
L. Michael3-Mar-14 21:41
L. Michael3-Mar-14 21:41 
GeneralRe: Nice article but .... Pin
Rahman Mahmoodi3-Mar-14 20:12
Rahman Mahmoodi3-Mar-14 20:12 
AnswerRe: Nice article but .... Pin
L. Michael10-Mar-15 23:25
L. Michael10-Mar-15 23:25 
GeneralMy vote of 5 Pin
Member 1050472121-Feb-14 21:26
Member 1050472121-Feb-14 21:26 
GeneralLooks great! Pin
Quentin in SA19-Feb-14 18:59
Quentin in SA19-Feb-14 18:59 
AnswerRe: Looks great! Pin
L. Michael19-Feb-14 21:26
L. Michael19-Feb-14 21:26 
Questionaren't you Pin
Sacha Barber19-Feb-14 6:14
Sacha Barber19-Feb-14 6:14 
AnswerRe: aren't you Pin
L. Michael19-Feb-14 9:43
L. Michael19-Feb-14 9:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.