From 3392d65fd1693e35cad587798158f7885ea22689 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Sun, 19 Jan 2025 16:10:45 +0100 Subject: [PATCH 1/3] Extract the Entity Framework configuration of aggregates into dedicated classes within the vertical slice feature namespace --- .../Database/AccountManagementDbContext.cs | 28 ------------------- .../Domain/LoginConfiguration.cs | 16 +++++++++++ .../Signups/Domain/SignupConfiguration.cs | 15 ++++++++++ .../Tenants/Domain/TenantConfiguration.cs | 14 ++++++++++ .../Users/Domain/UserConfiguration.cs | 22 +++++++++++++++ .../EntityFramework/ModelBuilderExtensions.cs | 21 ++++++-------- .../EntityFramework/SharedKernelDbContext.cs | 2 ++ 7 files changed, 78 insertions(+), 40 deletions(-) create mode 100644 application/account-management/Core/Features/Authentication/Domain/LoginConfiguration.cs create mode 100644 application/account-management/Core/Features/Signups/Domain/SignupConfiguration.cs create mode 100644 application/account-management/Core/Features/Tenants/Domain/TenantConfiguration.cs create mode 100644 application/account-management/Core/Features/Users/Domain/UserConfiguration.cs diff --git a/application/account-management/Core/Database/AccountManagementDbContext.cs b/application/account-management/Core/Database/AccountManagementDbContext.cs index 129678040..6762214d2 100644 --- a/application/account-management/Core/Database/AccountManagementDbContext.cs +++ b/application/account-management/Core/Database/AccountManagementDbContext.cs @@ -3,7 +3,6 @@ using PlatformPlatform.AccountManagement.Features.Signups.Domain; using PlatformPlatform.AccountManagement.Features.Tenants.Domain; using PlatformPlatform.AccountManagement.Features.Users.Domain; -using PlatformPlatform.SharedKernel.Domain; using PlatformPlatform.SharedKernel.EntityFramework; using PlatformPlatform.SharedKernel.ExecutionContext; @@ -19,31 +18,4 @@ public sealed class AccountManagementDbContext(DbContextOptions Tenants => Set(); public DbSet Users => Set(); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - // Login - modelBuilder.MapStronglyTypedId(t => t.Id); - modelBuilder.MapStronglyTypedId(u => u.TenantId); - modelBuilder.MapStronglyTypedUuid(u => u.UserId); - - // Signup - modelBuilder.MapStronglyTypedUuid(a => a.Id); - modelBuilder.MapStronglyTypedNullableId(u => u.TenantId); - - // Tenant - modelBuilder.MapStronglyTypedId(t => t.Id); - - // User - modelBuilder.MapStronglyTypedUuid(u => u.Id); - modelBuilder.MapStronglyTypedId(u => u.TenantId); - modelBuilder.Entity() - .OwnsOne(e => e.Avatar, b => b.ToJson()) - .HasOne() - .WithMany() - .HasForeignKey(u => u.TenantId) - .HasPrincipalKey(t => t.Id); - } } diff --git a/application/account-management/Core/Features/Authentication/Domain/LoginConfiguration.cs b/application/account-management/Core/Features/Authentication/Domain/LoginConfiguration.cs new file mode 100644 index 000000000..dd6c812e5 --- /dev/null +++ b/application/account-management/Core/Features/Authentication/Domain/LoginConfiguration.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PlatformPlatform.SharedKernel.Domain; +using PlatformPlatform.SharedKernel.EntityFramework; + +namespace PlatformPlatform.AccountManagement.Features.Authentication.Domain; + +public sealed class LoginConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.MapStronglyTypedId(t => t.Id); + builder.MapStronglyTypedId(u => u.TenantId); + builder.MapStronglyTypedUuid(u => u.UserId); + } +} diff --git a/application/account-management/Core/Features/Signups/Domain/SignupConfiguration.cs b/application/account-management/Core/Features/Signups/Domain/SignupConfiguration.cs new file mode 100644 index 000000000..2599dfbc6 --- /dev/null +++ b/application/account-management/Core/Features/Signups/Domain/SignupConfiguration.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PlatformPlatform.SharedKernel.Domain; +using PlatformPlatform.SharedKernel.EntityFramework; + +namespace PlatformPlatform.AccountManagement.Features.Signups.Domain; + +public sealed class SignupConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.MapStronglyTypedUuid(a => a.Id); + builder.MapStronglyTypedNullableId(u => u.TenantId); + } +} diff --git a/application/account-management/Core/Features/Tenants/Domain/TenantConfiguration.cs b/application/account-management/Core/Features/Tenants/Domain/TenantConfiguration.cs new file mode 100644 index 000000000..d070fd1cf --- /dev/null +++ b/application/account-management/Core/Features/Tenants/Domain/TenantConfiguration.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PlatformPlatform.SharedKernel.Domain; +using PlatformPlatform.SharedKernel.EntityFramework; + +namespace PlatformPlatform.AccountManagement.Features.Tenants.Domain; + +public sealed class TenantConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.MapStronglyTypedId(t => t.Id); + } +} diff --git a/application/account-management/Core/Features/Users/Domain/UserConfiguration.cs b/application/account-management/Core/Features/Users/Domain/UserConfiguration.cs new file mode 100644 index 000000000..13d414512 --- /dev/null +++ b/application/account-management/Core/Features/Users/Domain/UserConfiguration.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PlatformPlatform.AccountManagement.Features.Tenants.Domain; +using PlatformPlatform.SharedKernel.Domain; +using PlatformPlatform.SharedKernel.EntityFramework; + +namespace PlatformPlatform.AccountManagement.Features.Users.Domain; + +public sealed class UserConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.MapStronglyTypedUuid(u => u.Id); + builder.MapStronglyTypedId(u => u.TenantId); + builder + .OwnsOne(e => e.Avatar, b => b.ToJson()) + .HasOne() + .WithMany() + .HasForeignKey(u => u.TenantId) + .HasPrincipalKey(t => t.Id); + } +} diff --git a/application/shared-kernel/SharedKernel/EntityFramework/ModelBuilderExtensions.cs b/application/shared-kernel/SharedKernel/EntityFramework/ModelBuilderExtensions.cs index 4e56f1dd2..63509037c 100644 --- a/application/shared-kernel/SharedKernel/EntityFramework/ModelBuilderExtensions.cs +++ b/application/shared-kernel/SharedKernel/EntityFramework/ModelBuilderExtensions.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using PlatformPlatform.SharedKernel.StronglyTypedIds; @@ -11,37 +12,34 @@ public static class ModelBuilderExtensions /// This method is used to tell Entity Framework how to map a strongly typed ID to a SQL column using the /// underlying type of the strongly-typed ID. /// - public static void MapStronglyTypedLongId(this ModelBuilder modelBuilder, Expression> expression) + public static void MapStronglyTypedLongId(this EntityTypeBuilder builder, Expression> expression) where T : class where TId : StronglyTypedLongId { - modelBuilder - .Entity() + builder .Property(expression) .HasConversion(v => v.Value, v => (Activator.CreateInstance(typeof(TId), v) as TId)!); } - public static void MapStronglyTypedUuid(this ModelBuilder modelBuilder, Expression> expression) + public static void MapStronglyTypedUuid(this EntityTypeBuilder builder, Expression> expression) where T : class where TId : StronglyTypedUlid { - modelBuilder - .Entity() + builder .Property(expression) .HasConversion(v => v.Value, v => (Activator.CreateInstance(typeof(TId), v) as TId)!); } - public static void MapStronglyTypedId(this ModelBuilder modelBuilder, Expression> expression) + public static void MapStronglyTypedId(this EntityTypeBuilder builder, Expression> expression) where T : class where TValue : IComparable where TId : StronglyTypedId { - modelBuilder - .Entity() + builder .Property(expression) .HasConversion(v => v.Value, v => (Activator.CreateInstance(typeof(TId), v) as TId)!); } public static void MapStronglyTypedNullableId( - this ModelBuilder modelBuilder, + this EntityTypeBuilder builder, Expression> idExpression ) where T : class @@ -54,8 +52,7 @@ public static void MapStronglyTypedNullableId( var idCoalesceExpression = Expression.Lambda>(Expression.Coalesce(idValueProperty, nullConstant), idParameter); - modelBuilder - .Entity() + builder .Property(idExpression) .HasConversion(idCoalesceExpression!, v => Activator.CreateInstance(typeof(TId), v) as TId); } diff --git a/application/shared-kernel/SharedKernel/EntityFramework/SharedKernelDbContext.cs b/application/shared-kernel/SharedKernel/EntityFramework/SharedKernelDbContext.cs index 3e94414b5..80bf0f911 100644 --- a/application/shared-kernel/SharedKernel/EntityFramework/SharedKernelDbContext.cs +++ b/application/shared-kernel/SharedKernel/EntityFramework/SharedKernelDbContext.cs @@ -24,6 +24,8 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.ApplyConfigurationsFromAssembly(typeof(TContext).Assembly); + // Ensures that all enum properties are stored as strings in the database. modelBuilder.UseStringForEnums(); From b473914717caaa3cf460816078809297a6bd682c Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Sun, 19 Jan 2025 17:11:41 +0100 Subject: [PATCH 2/3] Add Humanizer dependency to SharedKernel to enable retrieval of plural table names --- application/Directory.Packages.props | 1 + application/shared-kernel/SharedKernel/SharedKernel.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/application/Directory.Packages.props b/application/Directory.Packages.props index 23114cb98..f4ea2367b 100644 --- a/application/Directory.Packages.props +++ b/application/Directory.Packages.props @@ -21,6 +21,7 @@ + diff --git a/application/shared-kernel/SharedKernel/SharedKernel.csproj b/application/shared-kernel/SharedKernel/SharedKernel.csproj index cf322645c..45a84e53e 100644 --- a/application/shared-kernel/SharedKernel/SharedKernel.csproj +++ b/application/shared-kernel/SharedKernel/SharedKernel.csproj @@ -23,6 +23,7 @@ + From 4a6c05a43610920272911f4c832a772fc19dd8db Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Sun, 19 Jan 2025 17:24:20 +0100 Subject: [PATCH 3/3] Replace DBSet properties on DBContext with generic registration of DBSets --- .../Core/Database/AccountManagementDbContext.cs | 15 +-------------- .../Features/Signups/Domain/SignupRepository.cs | 2 +- .../account-management/Tests/DatabaseSeeder.cs | 4 ++-- .../EntityFramework/SharedKernelDbContext.cs | 8 ++++++++ 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/application/account-management/Core/Database/AccountManagementDbContext.cs b/application/account-management/Core/Database/AccountManagementDbContext.cs index 6762214d2..5141ac3c9 100644 --- a/application/account-management/Core/Database/AccountManagementDbContext.cs +++ b/application/account-management/Core/Database/AccountManagementDbContext.cs @@ -1,21 +1,8 @@ using Microsoft.EntityFrameworkCore; -using PlatformPlatform.AccountManagement.Features.Authentication.Domain; -using PlatformPlatform.AccountManagement.Features.Signups.Domain; -using PlatformPlatform.AccountManagement.Features.Tenants.Domain; -using PlatformPlatform.AccountManagement.Features.Users.Domain; using PlatformPlatform.SharedKernel.EntityFramework; using PlatformPlatform.SharedKernel.ExecutionContext; namespace PlatformPlatform.AccountManagement.Database; public sealed class AccountManagementDbContext(DbContextOptions options, IExecutionContext executionContext) - : SharedKernelDbContext(options, executionContext) -{ - public DbSet Logins => Set(); - - public DbSet Signups => Set(); - - public DbSet Tenants => Set(); - - public DbSet Users => Set(); -} + : SharedKernelDbContext(options, executionContext); diff --git a/application/account-management/Core/Features/Signups/Domain/SignupRepository.cs b/application/account-management/Core/Features/Signups/Domain/SignupRepository.cs index a44d5d836..9bb2fc4d1 100644 --- a/application/account-management/Core/Features/Signups/Domain/SignupRepository.cs +++ b/application/account-management/Core/Features/Signups/Domain/SignupRepository.cs @@ -14,7 +14,7 @@ public sealed class SignupRepository(AccountManagementDbContext accountManagemen { public Signup[] GetByEmailOrTenantId(TenantId tenantId, string email) { - return accountManagementDbContext.Signups + return DbSet .Where(r => !r.Completed) .Where(r => r.TenantId == tenantId || r.Email == email.ToLowerInvariant()) .ToArray(); diff --git a/application/account-management/Tests/DatabaseSeeder.cs b/application/account-management/Tests/DatabaseSeeder.cs index 372e65aed..617813bf4 100644 --- a/application/account-management/Tests/DatabaseSeeder.cs +++ b/application/account-management/Tests/DatabaseSeeder.cs @@ -13,9 +13,9 @@ public sealed class DatabaseSeeder public DatabaseSeeder(AccountManagementDbContext accountManagementDbContext) { Tenant1 = Tenant.Create(new TenantId("tenant-1"), "owner@tenant-1.com"); - accountManagementDbContext.Tenants.AddRange(Tenant1); + accountManagementDbContext.Set().AddRange(Tenant1); User1 = User.Create(Tenant1.Id, "owner@tenant-1.com", UserRole.Owner, true, null); - accountManagementDbContext.Users.AddRange(User1); + accountManagementDbContext.Set().AddRange(User1); accountManagementDbContext.SaveChanges(); } diff --git a/application/shared-kernel/SharedKernel/EntityFramework/SharedKernelDbContext.cs b/application/shared-kernel/SharedKernel/EntityFramework/SharedKernelDbContext.cs index 80bf0f911..d7892993f 100644 --- a/application/shared-kernel/SharedKernel/EntityFramework/SharedKernelDbContext.cs +++ b/application/shared-kernel/SharedKernel/EntityFramework/SharedKernelDbContext.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using Humanizer; using Microsoft.EntityFrameworkCore; using PlatformPlatform.SharedKernel.Domain; using PlatformPlatform.SharedKernel.ExecutionContext; @@ -26,6 +27,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(TContext).Assembly); + // Set pluralized table names for all aggregates + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + var tableName = entityType.GetTableName()!.Pluralize(); + entityType.SetTableName(tableName); + } + // Ensures that all enum properties are stored as strings in the database. modelBuilder.UseStringForEnums();