Repository navigation
Expand file tree
/
Copy pathMigrationChains.cs
More file actions
129 lines (115 loc) · 6.5 KB
/
Copy pathMigrationChains.cs
File metadata and controls
129 lines (115 loc) · 6.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using LearnStack.Infrastructure.Persistence;
using LearnStack.Modules.Audit.Infrastructure.Persistence;
using LearnStack.Modules.Customization.Infrastructure.Persistence;
using LearnStack.Modules.Education.Infrastructure.Persistence;
using LearnStack.Modules.Identity.Infrastructure.Persistence;
using LearnStack.Modules.Tenancy.Infrastructure.Persistence;
using LearnStack.SharedKernel.Tenancy;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace LearnStack.Tests.Integration.Database;
/// <summary>
/// Every migration chain this repository deploys, applied in one place.
/// </summary>
/// <remarks>
/// <para>
/// <b>One place, because three fixtures had their own copy and the third was
/// wrong.</b> A fixture that applies a subset does not fail — it produces a
/// smaller schema, and every catalogue sweep, every count and every seed silently
/// narrows to it. That happened twice: <c>SchemaFixture</c> applied two of three
/// chains and eight assertions stopped covering the tables Packet 8 added, and
/// <c>TenantIsolationFixture</c> did the same and its seed then failed on a table
/// that did not exist. A chain that ships is a chain this applies.
/// </para>
/// <para>
/// The history table names come from the design-time factories, which are what
/// <c>dotnet ef</c> — and therefore <c>make migrate</c> — actually use. A fixture
/// repeating the literal would assert the name it wrote itself, and the deployment
/// path could drift underneath a green suite.
/// </para>
/// </remarks>
internal static class MigrationChains
{
/// <summary>Applies every chain, as <c>learnstack_migration</c>.</summary>
public static Task ApplyAllAsync(string migrationConnectionString) =>
ApplyAsync(migrationConnectionString, platformTargetMigration: null);
/// <summary>Applies every chain, pinning only Platform for an isolated historical migration proof.</summary>
public static Task ApplyThroughPlatformMigrationAsync(string migrationConnectionString, string platformTargetMigration)
{
ArgumentException.ThrowIfNullOrWhiteSpace(platformTargetMigration);
return ApplyAsync(migrationConnectionString, platformTargetMigration);
}
/// <summary>Retains the central chain order while proving an upgrade from the two invitation predecessors.</summary>
public static Task ApplyThroughInvitationPredecessorsAsync(string migrationConnectionString,
string platformTargetMigration, string identityTargetMigration)
{
ArgumentException.ThrowIfNullOrWhiteSpace(platformTargetMigration);
ArgumentException.ThrowIfNullOrWhiteSpace(identityTargetMigration);
return ApplyAsync(migrationConnectionString, platformTargetMigration, identityTargetMigration);
}
private static async Task ApplyAsync(string migrationConnectionString, string? platformTargetMigration,
string? identityTargetMigration = null)
{
await ApplyTenancyAsync(migrationConnectionString);
await using (var platform = new PlatformDbContext(
Options<PlatformDbContext>(migrationConnectionString, PlatformDbContextFactory.HistoryTable)))
{
if (platformTargetMigration is null) await platform.Database.MigrateAsync();
else await platform.GetService<IMigrator>().MigrateAsync(platformTargetMigration);
}
// Tenancy is first, and that order is not cosmetic: audit_config
// carries the schema's only foreign key crossing two chains, to `tenants`. A
// fixture that hand-orders what `make migrate` globs is how a suite goes green
// over a deployment path that cannot build the schema, so this method orders them
// the way the recipe does — Tenancy ahead of everything else
// (Standards 05 § Migrations).
await using var audit = new AuditDbContext(
Options<AuditDbContext>(migrationConnectionString, AuditDbContextFactory.HistoryTable),
StaticTenantContextAccessor.Unresolved);
await audit.Database.MigrateAsync();
// The central creation witnesses require the existing Audit schema.
await using var identity = new IdentityDbContext(
Options<IdentityDbContext>(migrationConnectionString, IdentityDbContextFactory.HistoryTable),
StaticTenantContextAccessor.Unresolved, EmptyIdentityScope.Instance);
if (identityTargetMigration is null) await identity.Database.MigrateAsync();
else await identity.GetService<IMigrator>().MigrateAsync(identityTargetMigration);
await using (var customization = new CustomizationDbContext(
Options<CustomizationDbContext>(
migrationConnectionString, CustomizationDbContextFactory.HistoryTable),
StaticTenantContextAccessor.Unresolved))
{
await customization.Database.MigrateAsync();
}
// Education's organization guards call the shared function owned by Tenancy.
// Its parent references remain inside Education (ADR-0003 Amendment 6).
await using var education = new EducationDbContext(
Options<EducationDbContext>(migrationConnectionString, EducationDbContextFactory.HistoryTable),
StaticTenantContextAccessor.Unresolved);
await education.Database.MigrateAsync();
}
/// <summary>Installs the shared scope function before a dependent chain is tested alone.</summary>
public static async Task ApplyTenancyAsync(string migrationConnectionString)
{
await using var tenancy = new TenancyDbContext(
Options<TenancyDbContext>(migrationConnectionString, TenancyDbContextFactory.HistoryTable),
StaticTenantContextAccessor.Unresolved);
await tenancy.Database.MigrateAsync();
}
/// <summary>The history table every chain declares, for a fixture that counts them.</summary>
public static readonly string[] HistoryTables =
[
TenancyDbContextFactory.HistoryTable,
PlatformDbContextFactory.HistoryTable,
CustomizationDbContextFactory.HistoryTable,
AuditDbContextFactory.HistoryTable,
EducationDbContextFactory.HistoryTable,
IdentityDbContextFactory.HistoryTable,
];
private static DbContextOptions<TContext> Options<TContext>(
string connectionString, string historyTable)
where TContext : DbContext =>
new DbContextOptionsBuilder<TContext>()
.UseNpgsql(connectionString, npgsql => npgsql.MigrationsHistoryTable(historyTable))
.Options;
}