🎯 Mastering the Specification Pattern in C#

Creating Clean, Dynamic, and Flexible Filter Without if-else!

DotNet Full Stack Dev
4 min readOct 29, 2024

If you’ve ever felt tangled up in complex filtering logic or drowning in if-else statements, then let me introduce you to a game-changing design pattern: the Specification Pattern. This pattern helps us design complex, reusable business rules in a clear and maintainable way by creating specifications that can be combined, reused, and extended.

Let’s break down what the Specification Pattern is, why it’s a powerhouse in building flexible conditions, and how to implement it with a step-by-step guide in C#. Get ready to simplify your code and gain “WOW!”-level flexibility with this pattern!

đź“ŚExplore more at: https://dotnet-fullstack-dev.blogspot.com/
🌟 Clapping would be appreciated! 🚀

đź’ˇ What is the Specification Pattern?

The Specification Pattern is a behavioral design pattern that allows you to encapsulate business rules and criteria into reusable objects, known as specifications. Each specification represents a particular rule or condition, which can be:

  • Simple (a single condition).
  • Composite (combined with other specifications using logical operations).

The real power of this pattern is the ability to dynamically combine specifications at runtime, giving you the flexibility to adapt to different filtering or validation needs without bloating your codebase with conditional logic.

✨ Benefits of the Specification Pattern

  1. Reusability: Write once, use anywhere. Specifications are modular and can be reused across the application.
  2. Flexibility: Combine multiple specifications dynamically without changing the core classes.
  3. Readability: Instead of multiple complex conditions, we get clear, self-explanatory specifications.
  4. Testability: Specifications can be unit-tested independently, making business rules easy to validate and maintain.

🔧 Implementing the Specification Pattern in C#

Imagine we have a list of Product objects, and we need to filter them based on various criteria, such as price range, availability, and category. Instead of writing complex filtering logic, we’ll create specifications that define each condition.

Step 1: Define the Entity Class

Let’s start with a simple Product class that we want to filter.

public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public bool InStock { get; set; }
public string Category { get; set; }
}

Step 2: Create the Specification Interface

We’ll create an interface, ISpecification<T>, that will define a method to check if an entity meets the criteria.

public interface ISpecification<T>
{
bool IsSatisfiedBy(T entity);
}

Step 3: Build Concrete Specifications

Now we’ll define specifications for filtering products based on Price, Availability, and Category.

public class PriceSpecification : ISpecification<Product>
{
private readonly decimal _minPrice;
private readonly decimal _maxPrice;

public PriceSpecification(decimal minPrice, decimal maxPrice)
{
_minPrice = minPrice;
_maxPrice = maxPrice;
}

public bool IsSatisfiedBy(Product product)
{
return product.Price >= _minPrice && product.Price <= _maxPrice;
}
}

public class InStockSpecification : ISpecification<Product>
{
public bool IsSatisfiedBy(Product product)
{
return product.InStock;
}
}

public class CategorySpecification : ISpecification<Product>
{
private readonly string _category;

public CategorySpecification(string category)
{
_category = category;
}

public bool IsSatisfiedBy(Product product)
{
return product.Category.Equals(_category, StringComparison.OrdinalIgnoreCase);
}
}

Each specification is responsible for evaluating a single condition, making our filtering logic modular and maintainable.

Step 4: Create Composite Specifications

Let’s add more power! We can create composite specifications by combining multiple specifications with logical operators like AND, OR, and NOT.

public class AndSpecification<T> : ISpecification<T>
{
private readonly ISpecification<T> _spec1;
private readonly ISpecification<T> _spec2;

public AndSpecification(ISpecification<T> spec1, ISpecification<T> spec2)
{
_spec1 = spec1;
_spec2 = spec2;
}

public bool IsSatisfiedBy(T entity)
{
return _spec1.IsSatisfiedBy(entity) && _spec2.IsSatisfiedBy(entity);
}
}

public class OrSpecification<T> : ISpecification<T>
{
private readonly ISpecification<T> _spec1;
private readonly ISpecification<T> _spec2;

public OrSpecification(ISpecification<T> spec1, ISpecification<T> spec2)
{
_spec1 = spec1;
_spec2 = spec2;
}

public bool IsSatisfiedBy(T entity)
{
return _spec1.IsSatisfiedBy(entity) || _spec2.IsSatisfiedBy(entity);
}
}

public class NotSpecification<T> : ISpecification<T>
{
private readonly ISpecification<T> _spec;

public NotSpecification(ISpecification<T> spec)
{
_spec = spec;
}

public bool IsSatisfiedBy(T entity)
{
return !_spec.IsSatisfiedBy(entity);
}
}

Now we can mix-and-match specifications with logical operations!

Step 5: Applying Specifications

Let’s create a scenario where we combine these specifications to filter a list of products.

public static void Main()
{
var products = new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 1200, InStock = true, Category = "Electronics" },
new Product { Id = 2, Name = "Phone", Price = 800, InStock = false, Category = "Electronics" },
new Product { Id = 3, Name = "Shoes", Price = 150, InStock = true, Category = "Apparel" },
new Product { Id = 4, Name = "TV", Price = 700, InStock = true, Category = "Electronics" }
};

// Define individual specifications
var inStockSpec = new InStockSpecification();
var electronicsCategorySpec = new CategorySpecification("Electronics");
var priceRangeSpec = new PriceSpecification(500, 1000);

// Combine specifications
var inStockAndElectronicsSpec = new AndSpecification<Product>(inStockSpec, electronicsCategorySpec);
var finalSpec = new AndSpecification<Product>(inStockAndElectronicsSpec, priceRangeSpec);

// Filter products based on combined specifications
var filteredProducts = products.Where(p => finalSpec.IsSatisfiedBy(p)).ToList();

foreach (var product in filteredProducts)
{
Console.WriteLine($"Product: {product.Name}, Price: {product.Price}, In Stock: {product.InStock}");
}
}

Expected Output:

Product: TV, Price: 700, In Stock: True

Here, the final specification (finalSpec) only includes products that are:

  1. In stock.
  2. Belong to the Electronics category.
  3. Priced between $500 and $1000.

This approach makes the filtering logic highly customizable and easy to understand.

🎉 Why the Specification Pattern is a Game-Changer!

With the Specification Pattern, we’ve achieved a scalable way to manage complex, evolving business logic in a clear and readable manner. Gone are the days of tangled if-else chains—this pattern brings:

  • Modularity: Separate conditions for easier maintenance.
  • Reusability: Apply specifications across different parts of your app.
  • Flexibility: Dynamically combine criteria at runtime.

So, if you’re working on a C# application that demands a lot of flexible filtering or validation, the Specification Pattern is your go-to solution for clean, maintainable, and scalable code.

--

--

DotNet Full Stack Dev
DotNet Full Stack Dev

Written by DotNet Full Stack Dev

Join me to master .NET Full Stack Development & boost your skills by 1% daily with insights, examples, and techniques! https://dotnet-fullstack-dev.blogspot.com

No responses yet