🎯 Mastering the Specification Pattern in C#
Creating Clean, Dynamic, and Flexible Filter Without if-else!
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
- Reusability: Write once, use anywhere. Specifications are modular and can be reused across the application.
- Flexibility: Combine multiple specifications dynamically without changing the core classes.
- Readability: Instead of multiple complex conditions, we get clear, self-explanatory specifications.
- 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:
- In stock.
- Belong to the Electronics category.
- 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.