إزاي تعمل Sorting في الـ API بتاعتك بشكل سهل ومرن؟

لما تيجي تتعامل مع APIs بيبقا في الغالب بيتم إرجاع البيانات من الـ Endpoint بنفس الترتيب اللي موجود في قاعدة البيانات، وده بيبقا غالبًا مبني على الـ Primary Key. لكن لو انت محتاج تدي المستخدم (زي Front-end أو Mobile Developer) إمكانية يعمل Sort على البيانات اللي بيرجعها الـ API، محتاج تضيف خاصية للـ Sorting بناءً على عمود معين زي Name أو Price.

الـ Scenario اللي عندنا

احنا عايزين نعدل الـ Endpoint بتاعنا بحيث يقدر ياخد Query Parameter من الـ URL بالشكل ده:

{{url}}/api/products?sort=name

الخطوات لتطبيق الـ Sorting:

  1. تعديل الـ Endpoint نفسه: نعدل الـ GetProducts method عشان تاخد sort كـ Parameter:
[HttpGet] // GET: /api/Products
public async Task<ActionResult<IReadOnlyList<ProductToReturnDto>>> GetProducts(string sort)
    {
        var spec = new ProductWithBrandAndCategorySpecifications(sort);
    }
    ```
    
هنا هنبعت قيمة الـ `sort` للـ Specification الخاصة بينا اللي هتتعامل مع الـ Sorting.
هنحتاج نغير الـ Specification بتاعنا انه ياخد Parameter نوعه String
في الـ [[Specification Design Pattern]] اللي علمناها في [[Refactor GetProducts and GetProduct using Specification DP]]
 
---
 
2. **تعديل الـ Specification Design Pattern**: بما إننا بنستخدم الـ `Specification Pattern` لتنظيم الـ Queries، هنعدل الـ Specification الحالية عشان تدعم الـ Sorting.
    
- عندنا حاليًا اتنين Specifications:
	- الـ`Criteria`: لتحديد الشرط.
	- الـ`Includes`: لإضافة الـ Relationships اللي محتاجينها (زي الـ `Brand` أو `Category`).
- في الـ LINQ عندنا 4 طرق للترتيب:
	
	- `OrderBy`
	- `OrderByDescending`
	- `ThenBy`
	- `ThenByDescending`
	
لكننا هنركز بس على أول اتنين، لأننا بنرتب بناءً على عمود واحد (زي `Name` أو `Price`).
 
 
لو مش فاكر تستخدم إيه، ارجع لـ Static Query زي دي:
 
```csharp
return (IEnumerable<T>) await _dbContext.Set<Product>().OrderBy(p => p.Name).Include(p => p.Brand);

  1. إضافة خصائص جديدة للـ Base Specification: هنحتاج نضيف خصائص جديدة للـ BaseSpecifications عشان تدعم الـ OrderBy و OrderByDescending.
  • هنعدل الـ Interface:
public interface ISpecifications<T> where T : BaseEntity
{
	Expression<Func<T, bool>> Criteria { get; set; }
	List<Expression<Func<T, object>>> Includes { get; set; }
 
	// New properties signutures
	Expression<Func<T, object>> OrderBy { get; set; }
	Expression<Func<T, object>> OrderByDesc { get; set; }
}
  • ثم نعدل الـ Class:
public class BaseSpecifications<T> : ISpecification<T> where T : BaseEntity
{
	public Expression<Func<T, bool>> Criteria { get; private set; }
	public List<Expression<Func<T, object>>> Includes { get; } = new List<Expression<Func<T, object>>>();
 
	public Expression<Func<T, object>> OrderBy { get; private set; }
	public Expression<Func<T, object>> OrderByDesc { get; private set; }
 
	public BaseSpecification()
	{
	}
 
    public BaseSpecification(Expression<Func<T, bool>> criteria)
    {
        Criteria = criteria;
    }
 
	// Two methods: To just SET
	public void AddOrderBy(Expression<Func<T, object>> orderByExpression)
	{
		OrderBy = orderByExpression;
	}
 
	public void AddOrderByDesc(Expression<Func<T, object>> orderByDescExpression)
	{
		OrderByDesc = orderByDescExpression;
	}
}

  1. تعديل الـ Specification اللي بنستخدمها: هنعدل الـ ProductWithBrandAndCategorySpecifications عشان تدعم الـ Sorting بناءً على قيمة الـ sort.
public class ProductWithBrandAndCategorySpecifications : BaseSpecifications<Product>
{
	public ProductWithBrandAndCategorySpecifications(string sort)
	{
		Includes.Add(p => p.Brand);
		Includes.Add(p => p.Category);
 
		if (!string.IsNullOrEmpty(sort))
		{
			switch (sort)
			{
				case "priceAsc":
					AddOrderBy(p => p.Price);
					break;
				case "priceDesc":
					AddOrderByDesc(p => p.Price);
					break;
				default:
					AddOrderBy(p => p.Name);
					break;
			}
		}
		else
		{
			AddOrderBy(p => p.Name);
		}
	}
}

  1. تعديل الـ Specifications Evaluator: هنعدل الطريقة اللي الـ Specification بتتطبق بيها عشان نضيف خطوة الترتيب (Sorting). وهنا لازم نطبق الترتيب بعد الـ Where:
internal static class SpecificationsEvaluator<T> where T : BaseEntity
{
	public static IQueryable<T> GetQuery(IQueryable<T> inputQuery, ISpecification<T> spec)
	{
		var query = inputQuery;
 
		// Apply Criteria
		if (spec.Criteria != null)
		{
			query = query.Where(spec.Criteria);
		}
 
		// Apply Order By
		if (spec.OrderBy != null)
		{
			query = query.OrderBy(spec.OrderBy);
		}
		else if (spec.OrderByDesc != null)
		{
			query = query.OrderByDescending(spec.OrderByDesc);
		}
 
		// Apply Includes
		query = spec.Includes.Aggregate(query, (current, include) => current.Include(include));
 
		return query;
	}
}

  1. نتيجة الـ Sorting:
    • لو تم الترتيب بناءً على Name، هترجع البيانات مرتبة أبجديًا.
    • لو اخترت الترتيب بـ Price (تصاعديًا أو تنازليًا)، الترتيب هيبقا بناءً على السعر فقط.

خلاصة:

  • أضفنا خاصية مرنة للـ Sorting باستخدام الـ Specification Pattern.
  • دعمنا OrderBy و OrderByDescending.
  • قمنا بتعديل الـ Specifications Evaluator لتطبيق الترتيب بعد الـ Where.

النتيجة النهائية هي API بتسمح للـ Front-end أو Mobile Developer بترتيب البيانات بسهولة عن طريق Query Parameter زي ?sort=name أو ?sort=priceDesc.