DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Specification Pattern P3

🔹 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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

🔹 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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🔹 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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🔹 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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🔹 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))
        {
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🔹 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);
}
Enter fullscreen mode Exit fullscreen mode

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)