Patterns: Specification

Published on Thursday, January 4, 2018

Specification pattern unites Domain-Driven Design, application architecture modeling and Entity Framework in C#. Specification pattern is designed to order business rules and connect our code to the business terms. This article shows an example of implementation with Entity Framework.

For begging, we need to describe our model:

public sealed class User
{
    public User()
    {
        Contacts = new List<Contact>();
    }
    public int Id { get; set; }

    public string Login { get; set; }

    public bool IsActive { get; set; }

    public ICollection<Contact> Contacts { get; }
}

public abstract class Contact
{
    public int Id { get; set; }

    public bool IsActive { get; set; }
}

public class Phone : Contact
{
    public string PhoneCode { get; set; }
    public string PhoneNumber { get; set; }
}

public class Email : Contact
{
    public string Value { get; set; }
}

It's simple, but good enough for example. Here we add some Entity Framework magic and create data context:

public sealed class Context : DbContext
{
    public Context(DbContextOptions builderOptions)
        :base(builderOptions)
    {
        
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<User>().HasMany(o => o.Contacts).WithOne();
        modelBuilder.Entity<Contact>()
            .HasDiscriminator<int>("ContactType")
            .HasValue<Phone>(1)
            .HasValue<Email>(2);
    }
}

Now we can use SQL Server as a storage.

Well-designed application requires separation of storage and model, so we introduce domain repository for User class:

public interface IUserRepository
{
    void Add(User user);
    User Get(int id);
}

public sealed class UserRepository : IUserRepository
{
    private readonly Context _context;

    public UserRepository(Context context)
    {
        _context = context;
    }

    public void Add(User user)
    {
        _context.Set<User>().Add(user);
    }

    public User Get(int id)
    {
        return _context.Set<User>().FirstOrDefault(o => o.Id == id);
    }
}

It looks like standard application, nothing special. Interesting part begins, when you need to query your storage. Let's look at the most common ways.

Query filters

  • Find all users with active phones
  • Find all users by login prefix
  • Find all users by phone number

IQueryable-way

The simplest way - expose method with IQueryable<User> and allow developer to query by any condition:

public interface IUserRepository
{
    void Add(User user);
    User Get(int id);
    IQueryable<User> Query();
}

public sealed class UserRepository : IUserRepository
{
    private readonly Context _context;

    public UserRepository(Context context)
    {
        _context = context;
    }

    public void Add(User user)
    {
        _context.Set<User>().Add(user);
    }

    public User Get(int id)
    {
        return _context.Set<User>().FirstOrDefault(o => o.Id == id);
    }

    public IQueryable<User> Query()
    {
        return _context.Set<User>();
    }
}

With Query method, we can filter users:

public static void Search(IUserRepository repository)
{
    var users1 = repository.Query()
        .Where(o => o.Contacts.Any(c => c is Phone && c.IsActive))
        .ToArray();

    var users2 = repository.Query()
        .Where(o => o.Login.StartsWith("log"))
        .ToArray();

    var users3 = repository.Query()
        .Where(o => o.Contacts.Any(c => c is Phone && (c as Phone).PhoneNumber == "111-22-33"))
        .ToArray();
}

This is fast and simple solution, but it has disadvantages:

  • We need to repeat code every time, when we need to reuse the condition. It's not very good technique: we can forget to change condition at some place and so on - all disadvantages of code repetition.
  • Business logic is distributed in code. We can't enumerate all business requirements in code, because all filters are used in different parts of application.
  • Hard to test. We can't check your business logic separately from storage.

Extensions-way

We can try to eliminate all disadvantages of previous way and incapsulate all query filter in some extensions class:

public static class UserQueryExtensions
{
    public static IEnumerable<User> FindWithActivePhones(this IQueryable<User> query)
    {
        return query.Where(o => o.Contacts.Any(c => c is Phone && c.IsActive))
            .AsEnumerable();
    }

    public static IEnumerable<User> FindWithLoginPrefix(this IQueryable<User> query, string prefix)
    {
        return query.Where(o => o.Login.StartsWith(prefix))
            .AsEnumerable();
    }

    public static IEnumerable<User> FindWithPhoneNumber(this IQueryable<User> query, string phoneNumber)
    {
        return query.Where(o => o.Contacts.Any(c => c is Phone && (c as Phone).PhoneNumber == phoneNumber))
            .AsEnumerable();
    }
}
public static void Search(IUserRepository repository)
{
    var users1 = repository.Query().FindWithActivePhones();

    var users2 = repository.Query().FindWithLoginPrefix("log");

    var users3 = repository.Query().FindWithPhoneNumber("111-22-33");
}

It looks much better! We don't repeat code anymore, we have all filters in one place. And also, it can be tested:

[Test]
public void FindWithActivePhonesTest()
{
    var user = new User
    {
        Id = 1,
        Login = "login"
    };
    user.Contacts.Add(new Phone { IsActive = true, PhoneNumber = "123-321" });


    var queryable = new[] { user }.AsQueryable();

    var result = queryable.FindWithActivePhones();

    CollectionAssert.AreEquivalent(new[] { 1 }, result.Select(o => o.Id));
}

Our repository still gives us Query method, that is not good, because, it leads to uncontrolled usage. So, we can move logic to repository.

Repository-way

Just move all query methods to repository:

public interface IUserRepository
{
    void Add(User user);
    User Get(int id);
    IEnumerable<User> FindWithActivePhones();
    IEnumerable<User> FindWithLoginPrefix(string prefix);
    IEnumerable<User> FindWithPhoneNumber(string phoneNumber);
}

public sealed class UserRepository : IUserRepository
{
    private readonly Context _context;

    public UserRepository(Context context)
    {
        _context = context;
    }

    public void Add(User user)
    {
        _context.Set<User>().Add(user);
    }

    public User Get(int id)
    {
        return _context.Set<User>().FirstOrDefault(o => o.Id == id);
    }

    public IEnumerable<User> FindWithActivePhones()
    {
        return _context.Set<User>().Where(o => o.Contacts.Any(c => c is Phone && c.IsActive))
            .AsEnumerable();
    }

    public IEnumerable<User> FindWithLoginPrefix(string prefix)
    {
        return _context.Set<User>().Where(o => o.Login.StartsWith(prefix))
            .AsEnumerable();
    }

    public IEnumerable<User> FindWithPhoneNumber(string phoneNumber)
    {
        return _context.Set<User>().Where(o => o.Contacts.Any(c => c is Phone && (c as Phone).PhoneNumber == phoneNumber))
            .AsEnumerable();
    }
}
public static void Search(IUserRepository repository)
{
    var users1 = repository.FindWithActivePhones();

    var users2 = repository.FindWithLoginPrefix("log");

    var users3 = repository.FindWithPhoneNumber("111-22-33");
}

No dangerous Query method. But it's hard to test - filter methods are strongly connected to storage and Entity Framework.

Also, filters can be used only in storage access. What if I want to check newly created entity through business rules? It's impossible now. With some additional code, it can be made with extensions method, but remember about exposing Query method.

Actually, we can stop here, because it's enough for 99% of cases. Read on, if you use Domain-Driven development, Entity Framework (or, another CRM) and need unit testing.

Specification-way

First of all, let's describe all requirements we want to get:

  • Keep business rules in one place and don't repeat
  • Filters are separated from storage (can be used outside of repository)
  • Filters can be easily tested

The heart of the pattern is specification - special object with business rule.

public interface ISpecification<T>
{
    Expression<Func<T, bool>> IsSatisfiedBy { get; }
}

Where T is a business entity class (User in our example). Repository has one method, that works with specification:

public interface IUserRepository
{
    void Add(User user);
    User Get(int id);
    IEnumerable<User> Get(ISpecification<User> specification);
}

public sealed class UserRepository : IUserRepository
{
    private readonly Context _context;

    public UserRepository(Context context)
    {
        _context = context;
    }

    public void Add(User user)
    {
        _context.Set<User>().Add(user);
    }

    public User Get(int id)
    {
        return _context.Set<User>().FirstOrDefault(o => o.Id == id);
    }

    public IEnumerable<User> Get(ISpecification<User> specification)
    {
        return _context.Set<User>()
            .Where(specification.IsSatisfiedBy)
            .AsEnumerable();
    }
}

Entity Framework allows us to use LINQ to SQL with extensions method. Here, we interested in Where method with filter predicate:

public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)

Implement our specifications:

public class ActivePhonesSpecification : ISpecification<User>
{
	public Expression<Func<User, bool>> IsSatisfiedBy =>
		o => o.Contacts.Any(c => c is Phone && c.IsActive);
}

public class LoginPrefixSpecification : ISpecification<User>
{
	public LoginPrefixSpecification(string prefix)
	{
		IsSatisfiedBy = o => o.Login.StartsWith(prefix);
	}
	public Expression<Func<User, bool>> IsSatisfiedBy { get; }
}

public class PhoneNumberSpecification:ISpecification<User>
{
	public PhoneNumberSpecification(string phoneNumber)
	{
		IsSatisfiedBy = o => o.Contacts.Any(c => c is Phone && (c as Phone).PhoneNumber == phoneNumber);
	}
	public Expression<Func<User, bool>> IsSatisfiedBy { get; }
}

Look, we describe our business rules in one place. They are separated from storage and we work with domain objects.

public static void Search(IUserRepository repository)
{
	var users1 = repository.Get(new ActivePhonesSpecification());

	var users2 = repository.Get(new LoginPrefixSpecification("log"));

	var users3 = repository.Get(new PhoneNumberSpecification("111-22-33"));
}

Tests:

[Test]
public void FindWithActivePhonesTest()
{
	var user = new User
	{
		Id = 1,
		Login = "login"
	};
	user.Contacts.Add(new Phone { IsActive = true, PhoneNumber = "123-321" });


	var queryable = new[] { user }.AsQueryable();

	var specification = new ActivePhonesSpecification();
	var result = queryable.Where(specification.IsSatisfiedBy);

	CollectionAssert.AreEquivalent(new[] { 1 }, result.Select(o => o.Id));
}

Working with objects without storage:

[Test]
public void FindWithActivePhonesTest()
{
	var user = new User
	{
		Id = 1,
		Login = "login"
	};
	user.Contacts.Add(new Phone { IsActive = true, PhoneNumber = "123-321" });

	var specification = new ActivePhonesSpecification();

	Assert.IsTrue(specification.IsSatisfiedBy.Compile().Invoke(user));
}

As an improvement, you can pre-compile method from expression, or make it lazy-loaded.

  • DDD describes models, repositories and rules
  • Entity Framework and LINQ make easy asses to data
  • Specification stores rules separated from storage layer

Links