Entity Framework  

Specification Pattern in C#

Imagine you are a developer responsible for maintaining an HR employee portal. This is the Employee class:

public class Employee
  {
      public int Id { get; set; }
      public string Name { get; set; } = string.Empty; 
      public bool IsRemote { get; set; }
      public int DepartmentID { get; set; } 

      public  override string ToString()
      {
          return $"Id: {Id}, Name: {Name}, IsRemote: {(IsRemote ? "Yes" : "No")}, DepartmentID: {DepartmentID}";
      }
}

One day, HR asks you to find employees whose names start with the letter “A”. You create a method like this:

public List<Employee> GetNameByInitial(string queryName) {
var result = _db.Employees
    .Where(e => e.Name.StartsWith(queryName))
    .ToList();

return result;
}

Then the next day, HR needs something like get employee by department, by if they are remote, by finance department and work from home etc.

Eventually, you will suffer a method explosion by keep creating new method

GetByIsRemote()

GetByDepartment()

GetDepartmentAndIsRemote()

GetBySalary()

IThe Specification Pattern represents domain business rules as reusable, composable objects.

The Specification Pattern represents domain business rules as reusable, composable objects.

Think of it this way:

  • Domain Business Logic answers “What are the rules?”

  • Specification Pattern answers “How do we represent those rules in code?”

In this example, the Employee class contains data such as Name, DepartmentID, and IsRemote. These properties allow us to define business rules that HR may use when searching for employees.

We start by creating one Specification class for each business rule. For example, one specification checks an employee’s name, another checks the department, and another checks whether the employee works remotely.

First, we create an abstract base class that all concrete Specification classes will inherit from.

 public abstract class BaseSpecification <T>
 {
     public abstract Expression<Func<T, bool>> Criteria { get; }

 }

As you can see, my base class is an abstract class and accepts a generic type parameter.

The Criteria property is overridden by each concrete Specification to define its own filtering rule.

What and Why Used Expression Tree

Notice that the Criteria property returns an Expression<Func<T, bool>> instead of a Func<T, bool>.

An Expression<Func<T, bool>> represents the filtering logic as an expression tree, not executable code.

You can think of Expression<Func<T, bool>> like this:

Instead of immediately executing a query in memory, such as filtering a list of employees in C#, EF Core builds an expression that describes the query.

When the query is executed, EF Core translates that expression into a SQL statement like:

“SELECT * FROM Employee WHERE DepartmentId = 1”

This SQL is then sent to the database server, where the filtering actually happens.

You might ask, why not use Func<T,bool>?

If we returned a Func<T, bool> instead, the delegate would execute in C# after the data had already been loaded into memory. By returning an Expression<Func<T, bool>>, EF Core can translate the expression into SQL so that the database performs the filtering before sending the results back.

Then, we move to our concrete business specification class.

public class NameSpecification : BaseSpecification<Employee>
{
    private readonly string _queryName;

    public NameSpecification(string queryName)
    {
        _queryName = queryName;
    }
    public override Expression<Func<Employee, bool>> Criteria => e => e.Name.StartsWith(_queryName, StringComparison.OrdinalIgnoreCase);


}

We start with the NameSpecification class. This specification defines a business rule that filters employees whose names start with a given string passed into the constructor.

You can now see that the Criteria property has been overridden with a lambda expression that defines the filtering rule. This expression checks whether an employee’s name starts with the provided value.

Notice that this specification does not execute any query. It only defines the rule. EF Core will later translate this expression into SQL when the query is executed.

Same thing goes to the Department and IsRemote specification class

public class DepartmentSpecification : BaseSpecification<Employee>
{
    private readonly int _departmentId;

    public DepartmentSpecification(int deptId)
    {
        _departmentId= deptId;
    }

    public override Expression<Func<Employee, bool>> Criteria => e=>e.DepartmentID == _departmentId;    
}
 public class IsRemoteSpecification : BaseSpecification<Employee>
 {
     private readonly bool _isRemote;

     public IsRemoteSpecification(bool isRemote)
     {
         _isRemote = isRemote;
     }
     public override Expression<Func<Employee, bool>> Criteria => e => e.IsRemote == _isRemote;
 }

For demonstration purposes, we use hardcoded in-memory data. However, the design is intended for EF Core, where the data would come from a database and the expressions would be translated into SQL queries.

 public static IQueryable<Employee> GetAll()
 {

     return new List<Employee> {

 new() { Id =  1, Name = "Alice Johnson", DepartmentID = 1,  IsRemote = true  },
 new() { Id =  2, Name = "Alan Martinez",  DepartmentID = 1,  IsRemote = false },
 new() { Id =  3, Name = "Bob White",   DepartmentID = 1,  IsRemote = true  },
 new() { Id =  4, Name = "Bella Lee",     DepartmentID = 2,    IsRemote = false },
 new() { Id =  5, Name = "Charlie Brown",     DepartmentID = 2,    IsRemote = true  },
 new() { Id =  6, Name = "Carmen Davis",   DepartmentID = 3,     IsRemote = false },
 new() { Id =  7, Name = "Dennis Wilson",  DepartmentID = 3,   IsRemote = false },
 new() { Id =  8, Name = "Diane Taylor",  DepartmentID = 4,   IsRemote = true  },
 new() { Id =  9, Name = "Eva Anderson", DepartmentID = 4,    IsRemote = false },
 new() { Id = 10, Name = "Ellen Thomas",   DepartmentID = 5,    IsRemote = true  },
 new() { Id = 11, Name = "Ed Jackson", DepartmentID = 5,    IsRemote = false },

  }.AsQueryable();
 }

Then the main class:



var employeesDB = EmployeeRepository.GetAll();//load the data


string nameStartWith = string.Empty;


Console.WriteLine("Please insert initial name");
nameStartWith= Console.ReadLine() ?? "A"; // default to "A" if empty

var query = new NameSpecification(nameStartWith);  // pass value into specification

var emplo = employeesDB
    .Where(query.Criteria) // Expression is translated into SQL by EF Core
    .ToList();

//print the employee after the filter
foreach (var employee in emplo)
{
    Console.WriteLine(employee.ToString());
}

Output:

Same with Department ID and remote employee


//filter by department 
Console.WriteLine("Please insert department ID (1-5)");
DepartmentId = Convert.ToInt32(Console.ReadLine());

var query = new DepartmentSpecification(DepartmentId);

var emplo = employeesDB
    .Where(query.Criteria)
    .ToList();


//or filter by remote employee
Console.WriteLine("Please insert if employee is remote (Y/N)");
var remoteInput = Console.ReadLine();
isRemote = remoteInput?.ToUpper() == "Y";


var query  = new IsRemoteSpecification(isRemote);

var emplo = employeesDB
    .Where(query.Criteria)
    .ToList();

 

Output:

Everything is working fine so far.

But you may notice a limitation: what if we need to combine multiple conditions? For example, retrieving employees whose names start with “A” and who belong to Department ID = 2?

At this point, a single specification is not enough. We need a way to combine multiple specifications together.

This is where we introduce a custom And method.

This is the beauty of the Specification Pattern — it allows us to build business rules as reusable components and compose them together like LEGO blocks to form more complex queries.

AND method

In order to build And method, we first created And property in base class

  public abstract class BaseSpecification<T>
  {
      public abstract Expression<Func<T, bool>> Criteria { get; }

      // AND
      public BaseSpecification<T> And(BaseSpecification<T> other)
          => new AndSpecification<T>(this, other);

//....

And the And Specification Class

public class AndSpecification<T> : BaseSpecification<T>
{
    private readonly BaseSpecification<T> _left;
    private readonly BaseSpecification<T> _right;

    public AndSpecification(BaseSpecification<T> left, BaseSpecification<T> right)
    {
        _left = left;
        _right = right;
    }

    public override Expression<Func<T, bool>> Criteria
    {
        get
        {
            var leftExpr = _left.Criteria;
            var rightExpr = _right.Criteria;

            var param = Expression.Parameter(typeof(T));

            var body = Expression.AndAlso(
                Expression.Invoke(leftExpr, param),
                Expression.Invoke(rightExpr, param)
            );

            return Expression.Lambda<Func<T, bool>>(body, param);
        }
    }


}

In the base class, the And property wrote

=> new AndSpecification<T>(this, other);

This line of code means take the current object, and combine it with another specification

this refers to the current instance of the class.

For example, this mean we create a expression of filter by name start with value provided by user

Console.WriteLine("Please insert initial name");
nameStartWith = Console.ReadLine() ?? "A";

var query = new NameSpecification(nameStartWith);

If we want to add another condition:


Console.WriteLine("Please insert department ID (1-5)");
DepartmentId = Convert.ToInt32(Console.ReadLine());

var deptID = new DepartmentSpecification(DepartmentId);


var query =
    new NameSpecification(nameStartWith)
        .And(new DepartmentSpecification(DepartmentId))//by using the And we created

inside And():

  • this = nameSpec

  • other = deptSpec

Internally, left refers to nameSpec, and right refers to deptSpec.

So in constructor

public AndSpecification(BaseSpecification<T> left, BaseSpecification<T> right)

left” is just: first argument passed in

right” is: second argument passed in

Each time we call And, we combine two specifications into a new one. Repeating this allows us to build complex conditions from simple building blocks.

This is how we combine multiple conditions dynamically.

Then we just use this And method in main class:



Console.WriteLine("Please insert initial name");
nameStartWith = Console.ReadLine() ?? "A";

Console.WriteLine("Please insert department ID (1-5)");
DepartmentId = Convert.ToInt32(Console.ReadLine());

Console.WriteLine("Please insert if employee is remote (Y/N)");
var remoteInput = Console.ReadLine();

isRemote = remoteInput?.ToUpper() == "Y";

 

var spec =
    new NameSpecification(nameStartWith)
        .And(new DepartmentSpecification(DepartmentId))
        .And(new IsRemoteSpecification(isRemote));




var emplo = employeesDB
    .Where(spec.Criteria)
    .ToList();


foreach (var employee in emplo)
{
    Console.WriteLine(employee.ToString());
}

Output:

The System.Linq.Expressions.Expression class also provides other operations that allow us to build more complex logic, such as:

  • Expression.OrElse → for combining conditions using OR

  • Expression.Not → for negating a condition

These building blocks allow us to extend the Specification Pattern to support OR and NOT logic in a similar way to how we implemented AND.

Another Name Filtering Method

Now let’s say the HR manager requests a new filtering rule: they want to filter employees whose names end with a specific value.

Do we go back to NameSpecification and modify it to support both “starts with” and “ends with” logic?

No, we don’t do that here in Specification Pattern.

Specification Pattern hold firmly the principle, “One Specification, One Business Rule”.

If that is the case, we need to create two name specification rule, of course, we should not name the specification that search name start with as NameSpecification anymore, we should have a better name, something like this:

public class NameStartsWithSpecification : BaseSpecification<Employee>
{
    private readonly string _value;

    public NameStartsWithSpecification(string value)
    {
        _value = value;
    }

    public override Expression<Func<Employee, bool>> Criteria
        => e => e.Name.StartsWith(_value, StringComparison.OrdinalIgnoreCase);
}
public class NameEndsWithSpecification : BaseSpecification<Employee>
{
    private readonly string _value;

    public NameEndsWithSpecification(string value)
    {
        _value = value;
    }

    public override Expression<Func<Employee, bool>> Criteria
        => e => e.Name.EndsWith(_value, StringComparison.OrdinalIgnoreCase);
}

Conclusion

The Specification Pattern helps us avoid method explosion by turning business rules into small, reusable building blocks instead of writing many repository methods.

Each specification represents a single rule (like name, department, or remote status), and we can combine them dynamically using And, Or, and Not to build complex queries.

This keeps our code clean, flexible, and easier to extend without modifying existing logic.

You may download my demo code via my Github.