refactor(persistence): separate storage models from domain entities
Introduce dedicated *Record POCOs (ReservationRecord, UserRecord, WorkplaceRecord) as EF Core targets, keeping domain entities free of persistence concerns. Static mapper classes handle conversion between layers, and repositories track (domain, record) pairs to sync mutations back before saving. Domain entities gain Reconstitute() factory methods to bypass validation when rehydrating from storage. Also fixes GetByEmployeeAsync to exclude past reservations (Date >= today). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,11 +13,21 @@ public class Reservation : Entity
|
||||
public ReservationStatus Status { get; private set; }
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Reservation() : base()
|
||||
public static Reservation Reconstitute(
|
||||
Guid id, Guid workplaceId, string employeeEmail, string employeeName,
|
||||
DateOnly date, ReservationStatus status, DateTime createdAt) =>
|
||||
new(id, workplaceId, employeeEmail, employeeName, date, status, createdAt);
|
||||
|
||||
private Reservation(
|
||||
Guid id, Guid workplaceId, string employeeEmail, string employeeName,
|
||||
DateOnly date, ReservationStatus status, DateTime createdAt) : base(id)
|
||||
{
|
||||
EmployeeEmail = string.Empty;
|
||||
EmployeeName = string.Empty;
|
||||
WorkplaceId = workplaceId;
|
||||
EmployeeEmail = employeeEmail;
|
||||
EmployeeName = employeeName;
|
||||
Date = date;
|
||||
Status = status;
|
||||
CreatedAt = createdAt;
|
||||
}
|
||||
|
||||
private Reservation(Guid workplaceId, string employeeEmail, string employeeName, DateOnly date)
|
||||
|
||||
@@ -10,11 +10,16 @@ public class User : Entity
|
||||
public bool IsApproved { get; private set; }
|
||||
public bool IsAdmin { get; private set; }
|
||||
|
||||
private User() : base()
|
||||
public static User Reconstitute(Guid id, string email, string name, string passwordHash, bool isApproved, bool isAdmin) =>
|
||||
new(id, email, name, passwordHash, isApproved, isAdmin);
|
||||
|
||||
private User(Guid id, string email, string name, string passwordHash, bool isApproved, bool isAdmin) : base(id)
|
||||
{
|
||||
Email = string.Empty;
|
||||
Name = string.Empty;
|
||||
PasswordHash = string.Empty;
|
||||
Email = email;
|
||||
Name = name;
|
||||
PasswordHash = passwordHash;
|
||||
IsApproved = isApproved;
|
||||
IsAdmin = isAdmin;
|
||||
}
|
||||
|
||||
private User(string email, string name, string passwordHash, bool isAdmin) : base()
|
||||
|
||||
@@ -8,10 +8,14 @@ public class Workplace : Entity
|
||||
public string Location { get; private set; }
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
private Workplace() : base()
|
||||
public static Workplace Reconstitute(Guid id, string name, string location, bool isActive) =>
|
||||
new(id, name, location, isActive);
|
||||
|
||||
private Workplace(Guid id, string name, string location, bool isActive) : base(id)
|
||||
{
|
||||
Name = string.Empty;
|
||||
Location = string.Empty;
|
||||
Name = name;
|
||||
Location = location;
|
||||
IsActive = isActive;
|
||||
}
|
||||
|
||||
public Workplace(string name, string location) : base()
|
||||
|
||||
120
src/backend/src/Randall.Infrastructure/Migrations/20260327154255_SeparateStorageModels.Designer.cs
generated
Normal file
120
src/backend/src/Randall.Infrastructure/Migrations/20260327154255_SeparateStorageModels.Designer.cs
generated
Normal file
@@ -0,0 +1,120 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Randall.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Randall.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260327154255_SeparateStorageModels")]
|
||||
partial class SeparateStorageModels
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||
|
||||
modelBuilder.Entity("Randall.Infrastructure.Persistence.Records.ReservationRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EmployeeEmail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EmployeeName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("WorkplaceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmployeeEmail", "Date");
|
||||
|
||||
b.HasIndex("WorkplaceId", "Date");
|
||||
|
||||
b.ToTable("Reservations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Randall.Infrastructure.Persistence.Records.UserRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsApproved")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Randall.Infrastructure.Persistence.Records.WorkplaceRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Workplaces", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Randall.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SeparateStorageModels : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace Randall.Infrastructure.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||
|
||||
modelBuilder.Entity("Randall.Domain.Reservations.Reservation", b =>
|
||||
modelBuilder.Entity("Randall.Infrastructure.Persistence.Records.ReservationRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -51,10 +51,10 @@ namespace Randall.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("WorkplaceId", "Date");
|
||||
|
||||
b.ToTable("Reservations");
|
||||
b.ToTable("Reservations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Randall.Domain.Users.User", b =>
|
||||
modelBuilder.Entity("Randall.Infrastructure.Persistence.Records.UserRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -85,10 +85,10 @@ namespace Randall.Infrastructure.Migrations
|
||||
b.HasIndex("Email")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
b.ToTable("Users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Randall.Domain.Workplaces.Workplace", b =>
|
||||
modelBuilder.Entity("Randall.Infrastructure.Persistence.Records.WorkplaceRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -109,7 +109,7 @@ namespace Randall.Infrastructure.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Workplaces");
|
||||
b.ToTable("Workplaces", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Randall.Domain.Reservations;
|
||||
using Randall.Domain.Users;
|
||||
using Randall.Domain.Workplaces;
|
||||
using Randall.Infrastructure.Persistence.Records;
|
||||
|
||||
namespace Randall.Infrastructure.Persistence;
|
||||
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<Workplace> Workplaces => Set<Workplace>();
|
||||
public DbSet<Reservation> Reservations => Set<Reservation>();
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<WorkplaceRecord> Workplaces => Set<WorkplaceRecord>();
|
||||
public DbSet<ReservationRecord> Reservations => Set<ReservationRecord>();
|
||||
public DbSet<UserRecord> Users => Set<UserRecord>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Workplace>(entity =>
|
||||
modelBuilder.Entity<WorkplaceRecord>(entity =>
|
||||
{
|
||||
entity.ToTable("Workplaces");
|
||||
entity.HasKey(w => w.Id);
|
||||
entity.Property(w => w.Name).IsRequired().HasMaxLength(100);
|
||||
entity.Property(w => w.Location).IsRequired().HasMaxLength(200);
|
||||
entity.Property(w => w.IsActive).IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Reservation>(entity =>
|
||||
modelBuilder.Entity<ReservationRecord>(entity =>
|
||||
{
|
||||
entity.ToTable("Reservations");
|
||||
entity.HasKey(r => r.Id);
|
||||
entity.Property(r => r.WorkplaceId).IsRequired();
|
||||
entity.Property(r => r.EmployeeEmail).IsRequired().HasMaxLength(200);
|
||||
@@ -35,8 +35,9 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
entity.HasIndex(r => new { r.EmployeeEmail, r.Date });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<User>(entity =>
|
||||
modelBuilder.Entity<UserRecord>(entity =>
|
||||
{
|
||||
entity.ToTable("Users");
|
||||
entity.HasKey(u => u.Id);
|
||||
entity.Property(u => u.Email).IsRequired().HasMaxLength(200);
|
||||
entity.Property(u => u.Name).IsRequired().HasMaxLength(200);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using Randall.Domain.Reservations;
|
||||
using Randall.Infrastructure.Persistence.Records;
|
||||
|
||||
namespace Randall.Infrastructure.Persistence.Mappers;
|
||||
|
||||
public static class ReservationMapper
|
||||
{
|
||||
public static Reservation ToDomain(ReservationRecord record) =>
|
||||
Reservation.Reconstitute(
|
||||
record.Id,
|
||||
record.WorkplaceId,
|
||||
record.EmployeeEmail,
|
||||
record.EmployeeName,
|
||||
record.Date,
|
||||
record.Status,
|
||||
record.CreatedAt);
|
||||
|
||||
public static ReservationRecord ToRecord(Reservation domain) =>
|
||||
new()
|
||||
{
|
||||
Id = domain.Id,
|
||||
WorkplaceId = domain.WorkplaceId,
|
||||
EmployeeEmail = domain.EmployeeEmail,
|
||||
EmployeeName = domain.EmployeeName,
|
||||
Date = domain.Date,
|
||||
Status = domain.Status,
|
||||
CreatedAt = domain.CreatedAt,
|
||||
};
|
||||
|
||||
public static void SyncToRecord(Reservation domain, ReservationRecord record)
|
||||
{
|
||||
record.Status = domain.Status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Randall.Domain.Users;
|
||||
using Randall.Infrastructure.Persistence.Records;
|
||||
|
||||
namespace Randall.Infrastructure.Persistence.Mappers;
|
||||
|
||||
public static class UserMapper
|
||||
{
|
||||
public static User ToDomain(UserRecord record) =>
|
||||
User.Reconstitute(
|
||||
record.Id,
|
||||
record.Email,
|
||||
record.Name,
|
||||
record.PasswordHash,
|
||||
record.IsApproved,
|
||||
record.IsAdmin);
|
||||
|
||||
public static UserRecord ToRecord(User domain) =>
|
||||
new()
|
||||
{
|
||||
Id = domain.Id,
|
||||
Email = domain.Email,
|
||||
Name = domain.Name,
|
||||
PasswordHash = domain.PasswordHash,
|
||||
IsApproved = domain.IsApproved,
|
||||
IsAdmin = domain.IsAdmin,
|
||||
};
|
||||
|
||||
public static void SyncToRecord(User domain, UserRecord record)
|
||||
{
|
||||
record.IsApproved = domain.IsApproved;
|
||||
record.IsAdmin = domain.IsAdmin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Randall.Domain.Workplaces;
|
||||
using Randall.Infrastructure.Persistence.Records;
|
||||
|
||||
namespace Randall.Infrastructure.Persistence.Mappers;
|
||||
|
||||
public static class WorkplaceMapper
|
||||
{
|
||||
public static Workplace ToDomain(WorkplaceRecord record) =>
|
||||
Workplace.Reconstitute(
|
||||
record.Id,
|
||||
record.Name,
|
||||
record.Location,
|
||||
record.IsActive);
|
||||
|
||||
public static WorkplaceRecord ToRecord(Workplace domain) =>
|
||||
new()
|
||||
{
|
||||
Id = domain.Id,
|
||||
Name = domain.Name,
|
||||
Location = domain.Location,
|
||||
IsActive = domain.IsActive,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Randall.Domain.Reservations;
|
||||
|
||||
namespace Randall.Infrastructure.Persistence.Records;
|
||||
|
||||
public class ReservationRecord
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid WorkplaceId { get; set; }
|
||||
public string EmployeeEmail { get; set; } = string.Empty;
|
||||
public string EmployeeName { get; set; } = string.Empty;
|
||||
public DateOnly Date { get; set; }
|
||||
public ReservationStatus Status { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Randall.Infrastructure.Persistence.Records;
|
||||
|
||||
public class UserRecord
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
public bool IsApproved { get; set; }
|
||||
public bool IsAdmin { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Randall.Infrastructure.Persistence.Records;
|
||||
|
||||
public class WorkplaceRecord
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Location { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
@@ -1,23 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Randall.Domain.Reservations;
|
||||
using Randall.Infrastructure.Persistence.Mappers;
|
||||
using Randall.Infrastructure.Persistence.Records;
|
||||
|
||||
namespace Randall.Infrastructure.Persistence.Reservations;
|
||||
|
||||
public class ReservationRepository(AppDbContext context) : IReservationRepository
|
||||
{
|
||||
public Task<Reservation?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
context.Reservations.FirstOrDefaultAsync(r => r.Id == id, ct);
|
||||
private readonly Dictionary<Guid, (Reservation Domain, ReservationRecord Record)> _tracked = [];
|
||||
|
||||
public async Task<IReadOnlyList<Reservation>> GetByEmployeeAsync(string employeeEmail, CancellationToken ct = default) =>
|
||||
await context.Reservations
|
||||
.Where(r => r.EmployeeEmail.ToLower() == employeeEmail.ToLower())
|
||||
public async Task<Reservation?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var record = await context.Reservations.FirstOrDefaultAsync(r => r.Id == id, ct);
|
||||
if (record is null) return null;
|
||||
var domain = ReservationMapper.ToDomain(record);
|
||||
_tracked[domain.Id] = (domain, record);
|
||||
return domain;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Reservation>> GetByEmployeeAsync(string employeeEmail, CancellationToken ct = default)
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var records = await context.Reservations
|
||||
.Where(r => r.EmployeeEmail.ToLower() == employeeEmail.ToLower() && r.Date >= today)
|
||||
.ToListAsync(ct);
|
||||
return records.Select(ReservationMapper.ToDomain).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Reservation>> GetByWorkplaceAndDateAsync(
|
||||
Guid workplaceId, DateOnly date, CancellationToken ct = default) =>
|
||||
await context.Reservations
|
||||
Guid workplaceId, DateOnly date, CancellationToken ct = default)
|
||||
{
|
||||
var records = await context.Reservations
|
||||
.Where(r => r.WorkplaceId == workplaceId && r.Date == date)
|
||||
.ToListAsync(ct);
|
||||
return records.Select(ReservationMapper.ToDomain).ToList();
|
||||
}
|
||||
|
||||
public Task<bool> ExistsActiveForEmployeeOnDateAsync(
|
||||
string employeeEmail, DateOnly date, CancellationToken ct = default) =>
|
||||
@@ -36,14 +53,24 @@ public class ReservationRepository(AppDbContext context) : IReservationRepositor
|
||||
ct);
|
||||
|
||||
public async Task<IReadOnlyList<Reservation>> GetActiveReservationsForDateAsync(
|
||||
DateOnly date, CancellationToken ct = default) =>
|
||||
await context.Reservations
|
||||
DateOnly date, CancellationToken ct = default)
|
||||
{
|
||||
var records = await context.Reservations
|
||||
.Where(r => r.Date == date && r.Status == ReservationStatus.Active)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task AddAsync(Reservation reservation, CancellationToken ct = default) =>
|
||||
await context.Reservations.AddAsync(reservation, ct);
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken ct = default) =>
|
||||
context.SaveChangesAsync(ct);
|
||||
return records.Select(ReservationMapper.ToDomain).ToList();
|
||||
}
|
||||
|
||||
public async Task AddAsync(Reservation reservation, CancellationToken ct = default)
|
||||
{
|
||||
var record = ReservationMapper.ToRecord(reservation);
|
||||
await context.Reservations.AddAsync(record, ct);
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken ct = default)
|
||||
{
|
||||
foreach (var (domain, record) in _tracked.Values)
|
||||
ReservationMapper.SyncToRecord(domain, record);
|
||||
return context.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Randall.Application.Common;
|
||||
using Randall.Domain.Users;
|
||||
using Randall.Domain.Workplaces;
|
||||
using Randall.Infrastructure.Persistence.Mappers;
|
||||
|
||||
namespace Randall.Infrastructure.Persistence.Seeding;
|
||||
|
||||
@@ -25,7 +26,7 @@ public static class DatabaseSeeder
|
||||
// Seed workplaces only if none exist yet
|
||||
if (!await context.Workplaces.AnyAsync())
|
||||
{
|
||||
var workplaces = ExpectedWorkplaces.Select(w => new Workplace(w.Name, w.Location));
|
||||
var workplaces = ExpectedWorkplaces.Select(w => WorkplaceMapper.ToRecord(new Workplace(w.Name, w.Location)));
|
||||
context.Workplaces.AddRange(workplaces);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
@@ -35,7 +36,7 @@ public static class DatabaseSeeder
|
||||
{
|
||||
var hash = passwordHasher.Hash(AdminPassword);
|
||||
var admin = User.Create(AdminEmail, "Admin", hash, isAdmin: true).Value!;
|
||||
context.Users.Add(admin);
|
||||
context.Users.Add(UserMapper.ToRecord(admin));
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,73 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Randall.Domain.Users;
|
||||
using Randall.Infrastructure.Persistence.Mappers;
|
||||
using Randall.Infrastructure.Persistence.Records;
|
||||
|
||||
namespace Randall.Infrastructure.Persistence.Users;
|
||||
|
||||
public class UserRepository(AppDbContext context) : IUserRepository
|
||||
{
|
||||
public Task<User?> GetByEmailAsync(string email, CancellationToken ct = default) =>
|
||||
context.Users.FirstOrDefaultAsync(u => u.Email == email.ToLowerInvariant(), ct);
|
||||
private readonly Dictionary<Guid, (User Domain, UserRecord Record)> _tracked = [];
|
||||
|
||||
public Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
context.Users.FirstOrDefaultAsync(u => u.Id == id, ct);
|
||||
private User Track(UserRecord record)
|
||||
{
|
||||
var domain = UserMapper.ToDomain(record);
|
||||
_tracked[domain.Id] = (domain, record);
|
||||
return domain;
|
||||
}
|
||||
|
||||
public async Task<User?> GetByEmailAsync(string email, CancellationToken ct = default)
|
||||
{
|
||||
var record = await context.Users.FirstOrDefaultAsync(u => u.Email == email.ToLowerInvariant(), ct);
|
||||
return record is null ? null : Track(record);
|
||||
}
|
||||
|
||||
public async Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var record = await context.Users.FirstOrDefaultAsync(u => u.Id == id, ct);
|
||||
return record is null ? null : Track(record);
|
||||
}
|
||||
|
||||
public Task<bool> ExistsByEmailAsync(string email, CancellationToken ct = default) =>
|
||||
context.Users.AnyAsync(u => u.Email == email.ToLowerInvariant(), ct);
|
||||
|
||||
public Task<List<User>> GetPendingAsync(CancellationToken ct = default) =>
|
||||
context.Users.Where(u => !u.IsApproved && !u.IsAdmin).ToListAsync(ct);
|
||||
public async Task<List<User>> GetPendingAsync(CancellationToken ct = default)
|
||||
{
|
||||
var records = await context.Users.Where(u => !u.IsApproved && !u.IsAdmin).ToListAsync(ct);
|
||||
return records.Select(UserMapper.ToDomain).ToList();
|
||||
}
|
||||
|
||||
public Task<List<User>> GetAllAsync(CancellationToken ct = default) =>
|
||||
context.Users.OrderBy(u => u.Name).ToListAsync(ct);
|
||||
public async Task<List<User>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
var records = await context.Users.OrderBy(u => u.Name).ToListAsync(ct);
|
||||
return records.Select(UserMapper.ToDomain).ToList();
|
||||
}
|
||||
|
||||
public Task<int> CountAdminsAsync(CancellationToken ct = default) =>
|
||||
context.Users.CountAsync(u => u.IsAdmin);
|
||||
|
||||
public Task<List<User>> GetAllNonAdminAsync(CancellationToken ct = default) =>
|
||||
context.Users.Where(u => !u.IsAdmin).OrderBy(u => u.Name).ToListAsync(ct);
|
||||
|
||||
public async Task AddAsync(User user, CancellationToken ct = default) =>
|
||||
await context.Users.AddAsync(user, ct);
|
||||
|
||||
public void Delete(User user) =>
|
||||
context.Users.Remove(user);
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken ct = default) =>
|
||||
context.SaveChangesAsync(ct);
|
||||
public async Task<List<User>> GetAllNonAdminAsync(CancellationToken ct = default)
|
||||
{
|
||||
var records = await context.Users.Where(u => !u.IsAdmin).OrderBy(u => u.Name).ToListAsync(ct);
|
||||
return records.Select(UserMapper.ToDomain).ToList();
|
||||
}
|
||||
|
||||
public async Task AddAsync(User user, CancellationToken ct = default)
|
||||
{
|
||||
var record = UserMapper.ToRecord(user);
|
||||
await context.Users.AddAsync(record, ct);
|
||||
}
|
||||
|
||||
public void Delete(User user)
|
||||
{
|
||||
if (_tracked.TryGetValue(user.Id, out var entry))
|
||||
context.Users.Remove(entry.Record);
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken ct = default)
|
||||
{
|
||||
foreach (var (domain, record) in _tracked.Values)
|
||||
UserMapper.SyncToRecord(domain, record);
|
||||
return context.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Randall.Domain.Workplaces;
|
||||
using Randall.Infrastructure.Persistence.Mappers;
|
||||
|
||||
namespace Randall.Infrastructure.Persistence.Workplaces;
|
||||
|
||||
public class WorkplaceRepository(AppDbContext context) : IWorkplaceRepository
|
||||
{
|
||||
public Task<Workplace?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
context.Workplaces.FirstOrDefaultAsync(w => w.Id == id, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Workplace>> GetAllActiveAsync(CancellationToken ct = default) =>
|
||||
await context.Workplaces.Where(w => w.IsActive).ToListAsync(ct);
|
||||
public async Task<Workplace?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var record = await context.Workplaces.FirstOrDefaultAsync(w => w.Id == id, ct);
|
||||
return record is null ? null : WorkplaceMapper.ToDomain(record);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Workplace>> GetAllActiveAsync(CancellationToken ct = default)
|
||||
{
|
||||
var records = await context.Workplaces.Where(w => w.IsActive).ToListAsync(ct);
|
||||
return records.Select(WorkplaceMapper.ToDomain).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user