🔹 Why Use the Specification Pattern?
❌ Problem Without It
- Our Generic Repository is limited to basic CRUD.
- We cannot filter products dynamically (e.g., Get all Nike products).
- We cannot sort (e.g., Order by Price).
- We cannot paginate (e.g., 10 products per page).
✅ Solution: Use the Specification Pattern
- The Specification Pattern allows us to encapsulate filtering logic in reusable objects.
- It makes our repository flexible, reusable, and scalable.
- It avoids repository bloat by keeping filtering logic separate.
📌 Step 3.1: Create ISpecification<T>
(Defines the Contract)
📂 Core/Specifications/ISpecification.cs
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Core.Specifications
{
public interface ISpecification<T>
{
Expression<Func<T, bool>>? Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
Expression<Func<T, object>>? OrderBy { get; }
Expression<Func<T, object>>? OrderByDescending { get; }
int? Take { get; }
int? Skip { get; }
bool IsPagingEnabled { get; }
}
}
🔹 What This Does
✔ Defines filtering (Criteria
), sorting (OrderBy
), and pagination (Skip
, Take
).
✔ Includes related entities (Includes
) for eager loading.
📌 Step 3.2: Create BaseSpecification<T>
(Stores Filtering Logic)
📂 Core/Specifications/BaseSpecification.cs
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Core.Specifications
{
public class BaseSpecification<T> : ISpecification<T>
{
public Expression<Func<T, bool>>? Criteria { get; }
public List<Expression<Func<T, object>>> Includes { get; } = new();
public Expression<Func<T, object>>? OrderBy { get; private set; }
public Expression<Func<T, object>>? OrderByDescending { get; private set; }
public int? Take { get; private set; }
public int? Skip { get; private set; }
public bool IsPagingEnabled { get; private set; }
public BaseSpecification(Expression<Func<T, bool>>? criteria = null)
{
Criteria = criteria;
}
public void AddInclude(Expression<Func<T, object>> includeExpression)
{
Includes.Add(includeExpression);
}
public void ApplyPaging(int skip, int take)
{
Skip = skip;
Take = take;
IsPagingEnabled = true;
}
public void ApplyOrderBy(Expression<Func<T, object>> orderByExpression)
{
OrderBy = orderByExpression;
}
public void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescExpression)
{
OrderByDescending = orderByDescExpression;
}
}
}
🔹 What This Does
✔ Stores filtering conditions dynamically.
✔ Supports sorting, eager loading, and pagination.
📌 Step 3.3: Create SpecificationEvaluator<T>
(Applies the Specification)
📂 Infrastructure/Data/SpecificationEvaluator.cs
using System.Linq;
using Core.Specifications;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Data
{
public class SpecificationEvaluator<T> where T : class
{
public static IQueryable<T> GetQuery(IQueryable<T> inputQuery, ISpecification<T> spec)
{
var query = inputQuery;
// Apply Filtering
if (spec.Criteria != null)
{
query = query.Where(spec.Criteria);
}
// Apply Sorting
if (spec.OrderBy != null)
{
query = query.OrderBy(spec.OrderBy);
}
else if (spec.OrderByDescending != null)
{
query = query.OrderByDescending(spec.OrderByDescending);
}
// Apply Pagination
if (spec.IsPagingEnabled)
{
query = query.Skip(spec.Skip!.Value).Take(spec.Take!.Value);
}
// Apply Includes (Eager Loading)
query = spec.Includes.Aggregate(query, (current, include) => current.Include(include));
return query;
}
}
}
🔹 What This Does
✔ Applies filtering (Where
).
✔ Applies sorting (OrderBy
, OrderByDescending
).
✔ Applies pagination (Skip
, Take
).
✔ Applies eager loading (Include
).
📌 Step 3.4: Modify GenericRepository<T>
to Support Specifications
📂 Infrastructure/Data/GenericRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using Core.Entities;
using Core.Interfaces;
using Core.Specifications;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Data
{
public class GenericRepository<T> : IGenericRepository<T> where T : BaseEntity
{
private readonly StoreContext _context;
public GenericRepository(StoreContext context)
{
_context = context;
}
public async Task<T?> GetByIdAsync(int id)
{
return await _context.Set<T>().FindAsync(id);
}
public async Task<IReadOnlyList<T>> ListAllAsync()
{
return await _context.Set<T>().ToListAsync();
}
public async Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec)
{
return await ApplySpecification(spec).ToListAsync();
}
public async Task<T?> GetEntityWithSpec(ISpecification<T> spec)
{
return await ApplySpecification(spec).FirstOrDefaultAsync();
}
private IQueryable<T> ApplySpecification(ISpecification<T> spec)
{
return SpecificationEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(), spec);
}
public void Add(T entity)
{
_context.Set<T>().Add(entity);
}
public void Update(T entity)
{
_context.Set<T>().Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
public void Remove(T entity)
{
_context.Set<T>().Remove(entity);
}
public async Task<bool> SaveAllAsync()
{
return await _context.SaveChangesAsync() > 0;
}
}
}
🔹 What This Does
✔ Supports filtering, sorting, and pagination via Specifications.
✔ No need to modify the repository for future queries.
📌 Step 3.5: Create ProductsWithFiltersSpecification
📂 Core/Specifications/ProductsWithFiltersSpecification.cs
namespace Core.Specifications
{
public class ProductsWithFiltersSpecification : BaseSpecification<Product>
{
public ProductsWithFiltersSpecification(string? brand, string? type)
: base(x =>
(string.IsNullOrEmpty(brand) || x.Brand == brand) &&
(string.IsNullOrEmpty(type) || x.Type == type))
{
}
}
}
🔹 What This Does
✔ Filters products dynamically based on Brand & Type.
📌 Step 3.6: Use the Specification in the Controller
📂 API/Controllers/ProductsController.cs
[HttpGet]
public async Task<ActionResult<List<Product>>> GetProducts(string? brand, string? type)
{
var spec = new ProductsWithFiltersSpecification(brand, type);
var products = await _repo.ListAsync(spec);
return Ok(products);
}
✔ Dynamically filters products based on request parameters.
🚀 Step 3 Complete!
✅ We implemented the Specification Pattern step by step.
✅ Now, our repository supports dynamic filtering, sorting, and pagination.
✅ The API can filter products by brand & type dynamically.
🔹 Next Steps
🚀 Extend the Specification Pattern to support sorting & pagination.
🚀 Test the API to confirm the Specification Pattern is working.
❓ Need More Clarification?
Let me know if any step needs more explanation! 😊
Top comments (0)