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! 😊

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay