Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion src/Buy2.Application/Common/Interfaces/IRepository.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Buy2.Application.Common.Specifications;
using Buy2.Domain.Entities;
using System.Linq.Expressions;

Expand All @@ -13,5 +14,60 @@ public interface IRepository<T> where T : class
Task AddRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default);
void Update(T entity);
void Delete(T entity);

// TEMPORARY default implementations for the stacked review of SCRUM-389.
// The specification-based reads are implemented by GenericRepository;
// the follow-up (SCRUM-390) turns these into abstract members and removes Query().
// Existing IRepository implementers (e.g. test fakes) keep compiling unaffected.
Task<T?> FirstOrDefaultAsync(ISpecification<T> specification, CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"{nameof(FirstOrDefaultAsync)} is implemented by GenericRepository.");

Task<T?> FirstOrDefaultAsync(
Expression<Func<T, bool>> predicate,
CancellationToken cancellationToken = default,
params string[] includes) =>
throw new NotImplementedException($"{nameof(FirstOrDefaultAsync)} is implemented by GenericRepository.");

Task<TResult?> FirstOrDefaultAsync<TResult>(
ISpecification<T> specification,
Expression<Func<T, TResult>> selector,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"{nameof(FirstOrDefaultAsync)} is implemented by GenericRepository.");

Task<List<T>> ListAsync(ISpecification<T> specification, CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"{nameof(ListAsync)} is implemented by GenericRepository.");

Task<List<T>> ListAsync(
Expression<Func<T, bool>>? predicate = null,
CancellationToken cancellationToken = default,
params string[] includes) =>
throw new NotImplementedException($"{nameof(ListAsync)} is implemented by GenericRepository.");

Task<List<TResult>> ListAsync<TResult>(
ISpecification<T> specification,
Expression<Func<T, TResult>> selector,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"{nameof(ListAsync)} is implemented by GenericRepository.");

Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null, CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"{nameof(CountAsync)} is implemented by GenericRepository.");

Task<int> CountAsync(ISpecification<T> specification, CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"{nameof(CountAsync)} is implemented by GenericRepository.");

Task<int> SumAsync(Expression<Func<T, bool>>? predicate, Expression<Func<T, int>> selector, CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"{nameof(SumAsync)} is implemented by GenericRepository.");

Task<int?> SumAsync(Expression<Func<T, bool>>? predicate, Expression<Func<T, int?>> selector, CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"{nameof(SumAsync)} is implemented by GenericRepository.");

Task<decimal> SumAsync(Expression<Func<T, bool>>? predicate, Expression<Func<T, decimal>> selector, CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"{nameof(SumAsync)} is implemented by GenericRepository.");

Task<decimal?> SumAsync(Expression<Func<T, bool>>? predicate, Expression<Func<T, decimal?>> selector, CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"{nameof(SumAsync)} is implemented by GenericRepository.");

Task<PagedResult<T>> PagedAsync(ISpecification<T> specification, int pageNumber, int pageSize, CancellationToken cancellationToken = default) =>
throw new NotImplementedException($"{nameof(PagedAsync)} is implemented by GenericRepository.");
}
}
}
129 changes: 129 additions & 0 deletions src/Buy2.Application/Common/Specifications/Specification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using System.Linq.Expressions;

namespace Buy2.Application.Common.Specifications;

/// <summary>
/// Persistence-agnostic sort instruction. Interpreted by the infrastructure
/// layer (EF Core today); application code never touches IQueryable.
/// </summary>
public sealed class Ordering<T>
{
public Expression<Func<T, object>> KeySelector { get; }
public bool Descending { get; }

public Ordering(Expression<Func<T, object>> keySelector, bool descending = false)
{
KeySelector = keySelector;
Descending = descending;
}
}

/// <summary>
/// Persistence-agnostic query description: filter + eager-load paths +
/// ordering + paging + tracking behavior. Lives in Application so handlers
/// compose queries without referencing any ORM.
/// Include paths use dotted navigation names, e.g. "JobRole.Department".
/// </summary>
public interface ISpecification<T> where T : class
{
Expression<Func<T, bool>>? Criteria { get; }
IReadOnlyList<string> Includes { get; }
IReadOnlyList<Ordering<T>> Orderings { get; }
int? Skip { get; }
int? Take { get; }
bool Tracked { get; }
bool IgnoreQueryFilters { get; }
}

public class Specification<T> : ISpecification<T> where T : class
{
private readonly List<Expression<Func<T, bool>>> _wheres = new();
private readonly List<string> _includes = new();
private readonly List<Ordering<T>> _orderings = new();

public Expression<Func<T, bool>>? Criteria =>
_wheres.Count == 0 ? null : _wheres.Aggregate(ExpressionCombiner.AndAlso);

public IReadOnlyList<string> Includes => _includes;
public IReadOnlyList<Ordering<T>> Orderings => _orderings;
public int? Skip { get; private set; }
public int? Take { get; private set; }
public bool Tracked { get; private set; }
public bool IgnoreQueryFilters { get; private set; }

public Specification<T> Where(Expression<Func<T, bool>> predicate)
{
_wheres.Add(predicate);
return this;
}

public Specification<T> Include(params string[] paths)
{
foreach (var path in paths)
{
if (!string.IsNullOrWhiteSpace(path) && !_includes.Contains(path))
{
_includes.Add(path);
}
}

return this;
}

public Specification<T> OrderBy(Expression<Func<T, object>> keySelector, bool descending = false)
{
_orderings.Add(new Ordering<T>(keySelector, descending));
return this;
}

public Specification<T> ThenBy(Expression<Func<T, object>> keySelector, bool descending = false)
{
return OrderBy(keySelector, descending);
}

public Specification<T> Page(int skip, int take)
{
Skip = skip;
Take = take;
return this;
}

public Specification<T> AsTracked()
{
Tracked = true;
return this;
}

public Specification<T> IgnoreFilters()
{
IgnoreQueryFilters = true;
return this;
}
}

/// <summary>
/// Paged query result. TotalCount is computed from the filter
/// without paging; Items contains only the requested page.
/// </summary>
public sealed record PagedResult<T>(
IReadOnlyList<T> Items,
int TotalCount,
int PageNumber,
int PageSize)
{
public int TotalPages => TotalCount == 0 ? 0 : (int)Math.Ceiling((double)TotalCount / PageSize);
}

internal static class ExpressionCombiner
{
public static Expression<Func<T, bool>> AndAlso<T>(
Expression<Func<T, bool>> left,
Expression<Func<T, bool>> right)
{
var parameter = Expression.Parameter(typeof(T), "e");
var body = Expression.AndAlso(
Expression.Invoke(left, parameter),
Expression.Invoke(right, parameter));
return Expression.Lambda<Func<T, bool>>(body, parameter);
}
}
8 changes: 7 additions & 1 deletion src/Buy2.Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Buy2.Application.Features.Points.Automation;
using Buy2.Application.Features.Points.Automation.Evaluators;
using Buy2.Application.Features.Schedules.ApplyTemplate.Services;
using Buy2.Application.Features.ShiftTemplates.UpdateShiftTemplate;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -11,12 +12,17 @@ public static class DependencyInjection
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));

services.AddScoped<IAutomationEvaluator, AttendanceAutomationEvaluator>();
services.AddScoped<IAutomationEvaluator, TaskAutomationEvaluator>();
services.AddScoped<IAutomationEvaluator, PerformanceAutomationEvaluator>();
services.AddScoped<IPointsAutomationRunner, PointsAutomationRunner>();
services.AddScoped<ShiftTemplateUpdateService>();
services.AddScoped<ITemplateApplicationLoader, TemplateApplicationLoader>();
services.AddScoped<IAvailabilityResolver, AvailabilityResolver>();
services.AddScoped<IEligibilityEvaluator, EligibilityEvaluator>();
services.AddScoped<IOverlapResolver, OverlapResolver>();
services.AddScoped<IScheduleAnalyticsService, ScheduleAnalyticsService>();

return services;
}
Expand Down
Loading
Loading