academia/src/backend/Adapters/Driven/Persistence/Repositories/StudentRepository.cs

128 lines
5.1 KiB
C#
Raw Normal View History

namespace Adapters.Driven.Persistence.Repositories;
using Adapters.Driven.Persistence.Context;
using Domain.Entities;
using Domain.Ports.Repositories;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
public class StudentRepository(AppDbContext context) : IStudentRepository
{
public async Task<Student?> GetByIdAsync(int id, CancellationToken ct = default) =>
await context.Students.FindAsync([id], ct);
public async Task<Student?> GetByIdWithEnrollmentsAsync(int id, CancellationToken ct = default) =>
await context.Students
.Include(s => s.Enrollments)
.ThenInclude(e => e.Subject)
.ThenInclude(sub => sub.Professor)
.AsSplitQuery() // Avoid cartesian explosion
.FirstOrDefaultAsync(s => s.Id == id, ct);
public async Task<IReadOnlyList<Student>> GetAllAsync(CancellationToken ct = default) =>
await context.Students.AsNoTracking().ToListAsync(ct);
public async Task<IReadOnlyList<Student>> GetAllWithEnrollmentsAsync(CancellationToken ct = default) =>
await context.Students
.Include(s => s.Enrollments)
.ThenInclude(e => e.Subject)
.ThenInclude(sub => sub.Professor)
.AsSplitQuery() // Avoid cartesian explosion
.AsNoTrackingWithIdentityResolution() // Better than AsNoTracking with navigations
.ToListAsync(ct);
public async Task<bool> ExistsAsync(int id, CancellationToken ct = default) =>
await CompiledQueries.StudentExists(context, id);
public async Task<bool> EmailExistsAsync(string email, int? excludeId = null, CancellationToken ct = default)
{
var normalizedEmail = email.Trim().ToLowerInvariant();
// Use raw SQL to avoid value object translation issues
// SQL Server requires column alias for SqlQueryRaw
var sql = excludeId.HasValue
? "SELECT 1 AS Value FROM Students WHERE LOWER(Email) = {0} AND Id != {1}"
: "SELECT 1 AS Value FROM Students WHERE LOWER(Email) = {0}";
var parameters = excludeId.HasValue
? new object[] { normalizedEmail, excludeId.Value }
: new object[] { normalizedEmail };
return await context.Database
.SqlQueryRaw<int>(sql, parameters)
.AnyAsync(ct);
}
// Direct projection - query only needed columns
public async Task<IReadOnlyList<TResult>> GetAllProjectedAsync<TResult>(
Expression<Func<Student, TResult>> selector,
CancellationToken ct = default) =>
await context.Students
.AsNoTracking()
.Select(selector)
.ToListAsync(ct);
public async Task<TResult?> GetByIdProjectedAsync<TResult>(
int id,
Expression<Func<Student, TResult>> selector,
CancellationToken ct = default) =>
await context.Students
.Where(s => s.Id == id)
.AsNoTracking()
.Select(selector)
.FirstOrDefaultAsync(ct);
// Keyset pagination - more efficient than OFFSET for large datasets
public async Task<(IReadOnlyList<TResult> Items, int? NextCursor, int TotalCount)> GetPagedProjectedAsync<TResult>(
Expression<Func<Student, TResult>> selector,
int? afterId = null,
int pageSize = 10,
CancellationToken ct = default)
{
var query = context.Students.AsNoTracking();
// Keyset: WHERE Id > afterId (uses index efficiently)
if (afterId.HasValue)
query = query.Where(s => s.Id > afterId.Value);
// Get one extra to determine if there's a next page
var items = await query
.OrderBy(s => s.Id)
.Take(pageSize + 1)
.Select(selector)
.ToListAsync(ct);
var hasMore = items.Count > pageSize;
var resultItems = hasMore ? items.Take(pageSize).ToList() : items;
// Get next cursor from last item (requires Id in projection or separate query)
int? nextCursor = null;
if (hasMore && resultItems.Count > 0)
{
var lastId = await context.Students
.AsNoTracking()
.Where(s => afterId.HasValue ? s.Id > afterId.Value : true)
.OrderBy(s => s.Id)
.Skip(pageSize - 1)
.Select(s => s.Id)
.FirstOrDefaultAsync(ct);
nextCursor = lastId > 0 ? lastId : null;
}
// Use compiled query for count
var totalCount = await CompiledQueries.CountStudents(context);
return (resultItems, nextCursor, totalCount);
}
public async Task<IReadOnlyList<Student>> GetPendingActivationAsync(CancellationToken ct = default) =>
await context.Students
.Where(s => s.ActivationCodeHash != null && s.ActivationExpiresAt > DateTime.UtcNow)
.AsNoTracking()
.ToListAsync(ct);
public void Add(Student student) => context.Students.Add(student);
public void Update(Student student) => context.Students.Update(student);
public void Delete(Student student) => context.Students.Remove(student);
}