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:
Robert van Diest
2026-03-27 16:45:20 +01:00
parent 4c71a1b4c8
commit 649c11b21a
17 changed files with 418 additions and 61 deletions

View File

@@ -13,11 +13,21 @@ public class Reservation : Entity
public ReservationStatus Status { get; private set; } public ReservationStatus Status { get; private set; }
public DateTime CreatedAt { get; private set; } public DateTime CreatedAt { get; private set; }
// EF Core constructor public static Reservation Reconstitute(
private Reservation() : base() 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; WorkplaceId = workplaceId;
EmployeeName = string.Empty; EmployeeEmail = employeeEmail;
EmployeeName = employeeName;
Date = date;
Status = status;
CreatedAt = createdAt;
} }
private Reservation(Guid workplaceId, string employeeEmail, string employeeName, DateOnly date) private Reservation(Guid workplaceId, string employeeEmail, string employeeName, DateOnly date)

View File

@@ -10,11 +10,16 @@ public class User : Entity
public bool IsApproved { get; private set; } public bool IsApproved { get; private set; }
public bool IsAdmin { 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; Email = email;
Name = string.Empty; Name = name;
PasswordHash = string.Empty; PasswordHash = passwordHash;
IsApproved = isApproved;
IsAdmin = isAdmin;
} }
private User(string email, string name, string passwordHash, bool isAdmin) : base() private User(string email, string name, string passwordHash, bool isAdmin) : base()

View File

@@ -8,10 +8,14 @@ public class Workplace : Entity
public string Location { get; private set; } public string Location { get; private set; }
public bool IsActive { 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; Name = name;
Location = string.Empty; Location = location;
IsActive = isActive;
} }
public Workplace(string name, string location) : base() public Workplace(string name, string location) : base()

View 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
}
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -17,7 +17,7 @@ namespace Randall.Infrastructure.Migrations
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); 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") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -51,10 +51,10 @@ namespace Randall.Infrastructure.Migrations
b.HasIndex("WorkplaceId", "Date"); 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") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -85,10 +85,10 @@ namespace Randall.Infrastructure.Migrations
b.HasIndex("Email") b.HasIndex("Email")
.IsUnique(); .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") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -109,7 +109,7 @@ namespace Randall.Infrastructure.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Workplaces"); b.ToTable("Workplaces", (string)null);
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@@ -1,28 +1,28 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Randall.Domain.Reservations; using Randall.Infrastructure.Persistence.Records;
using Randall.Domain.Users;
using Randall.Domain.Workplaces;
namespace Randall.Infrastructure.Persistence; namespace Randall.Infrastructure.Persistence;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options) public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{ {
public DbSet<Workplace> Workplaces => Set<Workplace>(); public DbSet<WorkplaceRecord> Workplaces => Set<WorkplaceRecord>();
public DbSet<Reservation> Reservations => Set<Reservation>(); public DbSet<ReservationRecord> Reservations => Set<ReservationRecord>();
public DbSet<User> Users => Set<User>(); public DbSet<UserRecord> Users => Set<UserRecord>();
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Entity<Workplace>(entity => modelBuilder.Entity<WorkplaceRecord>(entity =>
{ {
entity.ToTable("Workplaces");
entity.HasKey(w => w.Id); entity.HasKey(w => w.Id);
entity.Property(w => w.Name).IsRequired().HasMaxLength(100); entity.Property(w => w.Name).IsRequired().HasMaxLength(100);
entity.Property(w => w.Location).IsRequired().HasMaxLength(200); entity.Property(w => w.Location).IsRequired().HasMaxLength(200);
entity.Property(w => w.IsActive).IsRequired(); entity.Property(w => w.IsActive).IsRequired();
}); });
modelBuilder.Entity<Reservation>(entity => modelBuilder.Entity<ReservationRecord>(entity =>
{ {
entity.ToTable("Reservations");
entity.HasKey(r => r.Id); entity.HasKey(r => r.Id);
entity.Property(r => r.WorkplaceId).IsRequired(); entity.Property(r => r.WorkplaceId).IsRequired();
entity.Property(r => r.EmployeeEmail).IsRequired().HasMaxLength(200); 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 }); 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.HasKey(u => u.Id);
entity.Property(u => u.Email).IsRequired().HasMaxLength(200); entity.Property(u => u.Email).IsRequired().HasMaxLength(200);
entity.Property(u => u.Name).IsRequired().HasMaxLength(200); entity.Property(u => u.Name).IsRequired().HasMaxLength(200);

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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,
};
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -1,23 +1,40 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Randall.Domain.Reservations; using Randall.Domain.Reservations;
using Randall.Infrastructure.Persistence.Mappers;
using Randall.Infrastructure.Persistence.Records;
namespace Randall.Infrastructure.Persistence.Reservations; namespace Randall.Infrastructure.Persistence.Reservations;
public class ReservationRepository(AppDbContext context) : IReservationRepository public class ReservationRepository(AppDbContext context) : IReservationRepository
{ {
public Task<Reservation?> GetByIdAsync(Guid id, CancellationToken ct = default) => private readonly Dictionary<Guid, (Reservation Domain, ReservationRecord Record)> _tracked = [];
context.Reservations.FirstOrDefaultAsync(r => r.Id == id, ct);
public async Task<IReadOnlyList<Reservation>> GetByEmployeeAsync(string employeeEmail, CancellationToken ct = default) => public async Task<Reservation?> GetByIdAsync(Guid id, CancellationToken ct = default)
await context.Reservations {
.Where(r => r.EmployeeEmail.ToLower() == employeeEmail.ToLower()) 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); .ToListAsync(ct);
return records.Select(ReservationMapper.ToDomain).ToList();
}
public async Task<IReadOnlyList<Reservation>> GetByWorkplaceAndDateAsync( public async Task<IReadOnlyList<Reservation>> GetByWorkplaceAndDateAsync(
Guid workplaceId, DateOnly date, CancellationToken ct = default) => Guid workplaceId, DateOnly date, CancellationToken ct = default)
await context.Reservations {
var records = await context.Reservations
.Where(r => r.WorkplaceId == workplaceId && r.Date == date) .Where(r => r.WorkplaceId == workplaceId && r.Date == date)
.ToListAsync(ct); .ToListAsync(ct);
return records.Select(ReservationMapper.ToDomain).ToList();
}
public Task<bool> ExistsActiveForEmployeeOnDateAsync( public Task<bool> ExistsActiveForEmployeeOnDateAsync(
string employeeEmail, DateOnly date, CancellationToken ct = default) => string employeeEmail, DateOnly date, CancellationToken ct = default) =>
@@ -36,14 +53,24 @@ public class ReservationRepository(AppDbContext context) : IReservationRepositor
ct); ct);
public async Task<IReadOnlyList<Reservation>> GetActiveReservationsForDateAsync( public async Task<IReadOnlyList<Reservation>> GetActiveReservationsForDateAsync(
DateOnly date, CancellationToken ct = default) => DateOnly date, CancellationToken ct = default)
await context.Reservations {
var records = await context.Reservations
.Where(r => r.Date == date && r.Status == ReservationStatus.Active) .Where(r => r.Date == date && r.Status == ReservationStatus.Active)
.ToListAsync(ct); .ToListAsync(ct);
return records.Select(ReservationMapper.ToDomain).ToList();
}
public async Task AddAsync(Reservation reservation, CancellationToken ct = default) => public async Task AddAsync(Reservation reservation, CancellationToken ct = default)
await context.Reservations.AddAsync(reservation, ct); {
var record = ReservationMapper.ToRecord(reservation);
await context.Reservations.AddAsync(record, ct);
}
public Task SaveChangesAsync(CancellationToken ct = default) => public Task SaveChangesAsync(CancellationToken ct = default)
context.SaveChangesAsync(ct); {
foreach (var (domain, record) in _tracked.Values)
ReservationMapper.SyncToRecord(domain, record);
return context.SaveChangesAsync(ct);
}
} }

View File

@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
using Randall.Application.Common; using Randall.Application.Common;
using Randall.Domain.Users; using Randall.Domain.Users;
using Randall.Domain.Workplaces; using Randall.Domain.Workplaces;
using Randall.Infrastructure.Persistence.Mappers;
namespace Randall.Infrastructure.Persistence.Seeding; namespace Randall.Infrastructure.Persistence.Seeding;
@@ -25,7 +26,7 @@ public static class DatabaseSeeder
// Seed workplaces only if none exist yet // Seed workplaces only if none exist yet
if (!await context.Workplaces.AnyAsync()) 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); context.Workplaces.AddRange(workplaces);
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
@@ -35,7 +36,7 @@ public static class DatabaseSeeder
{ {
var hash = passwordHasher.Hash(AdminPassword); var hash = passwordHasher.Hash(AdminPassword);
var admin = User.Create(AdminEmail, "Admin", hash, isAdmin: true).Value!; var admin = User.Create(AdminEmail, "Admin", hash, isAdmin: true).Value!;
context.Users.Add(admin); context.Users.Add(UserMapper.ToRecord(admin));
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
} }

View File

@@ -1,37 +1,73 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Randall.Domain.Users; using Randall.Domain.Users;
using Randall.Infrastructure.Persistence.Mappers;
using Randall.Infrastructure.Persistence.Records;
namespace Randall.Infrastructure.Persistence.Users; namespace Randall.Infrastructure.Persistence.Users;
public class UserRepository(AppDbContext context) : IUserRepository public class UserRepository(AppDbContext context) : IUserRepository
{ {
public Task<User?> GetByEmailAsync(string email, CancellationToken ct = default) => private readonly Dictionary<Guid, (User Domain, UserRecord Record)> _tracked = [];
context.Users.FirstOrDefaultAsync(u => u.Email == email.ToLowerInvariant(), ct);
public Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default) => private User Track(UserRecord record)
context.Users.FirstOrDefaultAsync(u => u.Id == id, ct); {
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) => public Task<bool> ExistsByEmailAsync(string email, CancellationToken ct = default) =>
context.Users.AnyAsync(u => u.Email == email.ToLowerInvariant(), ct); context.Users.AnyAsync(u => u.Email == email.ToLowerInvariant(), ct);
public Task<List<User>> GetPendingAsync(CancellationToken ct = default) => public async Task<List<User>> GetPendingAsync(CancellationToken ct = default)
context.Users.Where(u => !u.IsApproved && !u.IsAdmin).ToListAsync(ct); {
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) => public async Task<List<User>> GetAllAsync(CancellationToken ct = default)
context.Users.OrderBy(u => u.Name).ToListAsync(ct); {
var records = await context.Users.OrderBy(u => u.Name).ToListAsync(ct);
return records.Select(UserMapper.ToDomain).ToList();
}
public Task<int> CountAdminsAsync(CancellationToken ct = default) => public Task<int> CountAdminsAsync(CancellationToken ct = default) =>
context.Users.CountAsync(u => u.IsAdmin); context.Users.CountAsync(u => u.IsAdmin);
public Task<List<User>> GetAllNonAdminAsync(CancellationToken ct = default) => public async Task<List<User>> GetAllNonAdminAsync(CancellationToken ct = default)
context.Users.Where(u => !u.IsAdmin).OrderBy(u => u.Name).ToListAsync(ct); {
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) => public async Task AddAsync(User user, CancellationToken ct = default)
await context.Users.AddAsync(user, ct); {
var record = UserMapper.ToRecord(user);
await context.Users.AddAsync(record, ct);
}
public void Delete(User user) => public void Delete(User user)
context.Users.Remove(user); {
if (_tracked.TryGetValue(user.Id, out var entry))
context.Users.Remove(entry.Record);
}
public Task SaveChangesAsync(CancellationToken ct = default) => public Task SaveChangesAsync(CancellationToken ct = default)
context.SaveChangesAsync(ct); {
foreach (var (domain, record) in _tracked.Values)
UserMapper.SyncToRecord(domain, record);
return context.SaveChangesAsync(ct);
}
} }

View File

@@ -1,13 +1,20 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Randall.Domain.Workplaces; using Randall.Domain.Workplaces;
using Randall.Infrastructure.Persistence.Mappers;
namespace Randall.Infrastructure.Persistence.Workplaces; namespace Randall.Infrastructure.Persistence.Workplaces;
public class WorkplaceRepository(AppDbContext context) : IWorkplaceRepository public class WorkplaceRepository(AppDbContext context) : IWorkplaceRepository
{ {
public Task<Workplace?> GetByIdAsync(Guid id, CancellationToken ct = default) => public async Task<Workplace?> GetByIdAsync(Guid id, CancellationToken ct = default)
context.Workplaces.FirstOrDefaultAsync(w => w.Id == id, ct); {
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) => public async Task<IReadOnlyList<Workplace>> GetAllActiveAsync(CancellationToken ct = default)
await context.Workplaces.Where(w => w.IsActive).ToListAsync(ct); {
var records = await context.Workplaces.Where(w => w.IsActive).ToListAsync(ct);
return records.Select(WorkplaceMapper.ToDomain).ToList();
}
} }