Skip to content

Commit

Permalink
✨ 226 Run Migrations and Seed Data via Hosted Service (#386)
Browse files Browse the repository at this point in the history
* WIP

* Added MigrationService.csproj

* Removed Database.csproj and refactored integration tests initialization logic

* Downgrade todo to note

* Update tools/MigrationService/Program.cs

Co-authored-by: Matthew Parker [SSW] <[email protected]>

---------

Co-authored-by: Matthew Parker [SSW] <[email protected]>
  • Loading branch information
danielmackay and MattParkerDev authored Oct 23, 2024
1 parent 63e90fa commit 986c62e
Show file tree
Hide file tree
Showing 19 changed files with 197 additions and 145 deletions.
14 changes: 7 additions & 7 deletions SSW.CleanArchitecture.sln
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.IntegrationTests", "
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{7AFFF91C-498F-4CE9-9DCB-E2FDBA18E81A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Database", "tools\Database\Database.csproj", "{08A4FCE5-D49A-4FC1-BB0B-3206326157BD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "tools\AppHost\AppHost.csproj", "{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MigrationService", "tools\MigrationService\MigrationService.csproj", "{7162F599-B39F-4C4B-BA1D-30500E38D117}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -73,14 +73,14 @@ Global
{6FAECF07-C024-45F6-B78A-A2771833F867}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FAECF07-C024-45F6-B78A-A2771833F867}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FAECF07-C024-45F6-B78A-A2771833F867}.Release|Any CPU.Build.0 = Release|Any CPU
{08A4FCE5-D49A-4FC1-BB0B-3206326157BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{08A4FCE5-D49A-4FC1-BB0B-3206326157BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08A4FCE5-D49A-4FC1-BB0B-3206326157BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08A4FCE5-D49A-4FC1-BB0B-3206326157BD}.Release|Any CPU.Build.0 = Release|Any CPU
{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E}.Release|Any CPU.Build.0 = Release|Any CPU
{7162F599-B39F-4C4B-BA1D-30500E38D117}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7162F599-B39F-4C4B-BA1D-30500E38D117}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7162F599-B39F-4C4B-BA1D-30500E38D117}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7162F599-B39F-4C4B-BA1D-30500E38D117}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -93,8 +93,8 @@ Global
{526EDF1F-026F-41EC-9FC5-433243492515} = {A388D69B-2789-4D2E-A6D9-331D8571DA7E}
{E631C496-2285-4B0E-8C04-EB9D0766D01A} = {A388D69B-2789-4D2E-A6D9-331D8571DA7E}
{6FAECF07-C024-45F6-B78A-A2771833F867} = {A388D69B-2789-4D2E-A6D9-331D8571DA7E}
{08A4FCE5-D49A-4FC1-BB0B-3206326157BD} = {7AFFF91C-498F-4CE9-9DCB-E2FDBA18E81A}
{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E} = {7AFFF91C-498F-4CE9-9DCB-E2FDBA18E81A}
{7162F599-B39F-4C4B-BA1D-30500E38D117} = {7AFFF91C-498F-4CE9-9DCB-E2FDBA18E81A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8975F724-46D1-4802-8C4B-6B386B69846F}
Expand Down
2 changes: 1 addition & 1 deletion tests/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8"/>
<PackageVersion Include="coverlet.collector" Version="6.0.0"/>
<PackageVersion Include="NetArchTest.Rules" Version="1.3.2"/>
<PackageVersion Include="Bogus" Version="34.0.2"/>
<PackageVersion Include="Bogus" Version="35.6.1"/>
<PackageVersion Include="NSubstitute" Version="5.1.0"/>
<PackageVersion Include="Respawn" Version="6.2.1"/>
<PackageVersion Include="Testcontainers" Version="3.10.0"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SSW.CleanArchitecture.Database;
using MigrationService.Initializers;
using SSW.CleanArchitecture.WebApi;

namespace WebApi.IntegrationTests.Common.Fixtures;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using MigrationService.Initializers;
using Respawn;
using SSW.CleanArchitecture.Database;

namespace WebApi.IntegrationTests.Common.Fixtures;

Expand All @@ -26,9 +26,10 @@ public async Task InitializeAsync()

// Create and seed database
using var scope = ScopeFactory.CreateScope();
var initializer = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitializer>();
await initializer.InitializeAsync();
// await initializer.SeedAsync();
var warehouseInitializer = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitializer>();
await warehouseInitializer.EnsureDatabaseAsync(default);
await warehouseInitializer.RunMigrationAsync(default);
// await warehouseInitializer.SeedDataAsync(default);

// NOTE: If there are any tables you want to skip being reset, they can be configured here
_checkpoint = await Respawner.CreateAsync(ConnectionString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\WebApi\WebApi.csproj" />
<ProjectReference Include="..\..\tools\Database\Database.csproj" />
<ProjectReference Include="..\..\tools\MigrationService\MigrationService.csproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions tools/AppHost/AppHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\WebApi\WebApi.csproj" />
<ProjectReference Include="..\MigrationService\MigrationService.csproj" />
</ItemGroup>

</Project>
20 changes: 9 additions & 11 deletions tools/AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,22 @@

var builder = DistributedApplication.CreateBuilder(args);

var db = builder
// NOTE: Double check that persistent DB code works
var sqlServer = builder
.AddSqlServer("sql")
.WithLifetime(ContainerLifetime.Persistent)
.WithLifetime(ContainerLifetime.Persistent);

var db = sqlServer
.AddDatabase("clean-architecture");

// TODO: https://github.com/SSWConsulting/SSW.CleanArchitecture/issues/226
// var migrationService = builder.AddProject<MigrationService>("migrations")
// .WithReference(warehouseDb)
// .WithReference(catalogDb)
// .WithReference(customersDb)
// .WithReference(ordersDb)
// .WaitFor(sqlServer);
var migrationService = builder.AddProject<MigrationService>("migrations")
.WithReference(db)
.WaitFor(sqlServer);

builder
.AddProject<WebApi>("api")
.WithExternalHttpEndpoints()
.WithReference(db)
// .WaitForCompletion(migrationService)
;
.WaitForCompletion(migrationService);

builder.Build().Run();
25 changes: 0 additions & 25 deletions tools/Database/Database.csproj

This file was deleted.

8 changes: 0 additions & 8 deletions tools/Database/MockCurrentUserServiceProvider.cs

This file was deleted.

40 changes: 0 additions & 40 deletions tools/Database/Program.cs

This file was deleted.

11 changes: 0 additions & 11 deletions tools/Database/appsettings.json

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
using Bogus;
using Bogus;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SSW.CleanArchitecture.Domain.Heroes;
using SSW.CleanArchitecture.Domain.Teams;
using SSW.CleanArchitecture.Infrastructure.Persistence;

namespace SSW.CleanArchitecture.Database;
namespace MigrationService.Initializers;

public class ApplicationDbContextInitializer(
ILogger<ApplicationDbContextInitializer> logger,
ApplicationDbContext dbContext)
public class ApplicationDbContextInitializer : DbContextInitializerBase<ApplicationDbContext>
{
private const int NumHeroes = 20;

private const int NumTeams = 5;

private readonly string[] _superHeroNames =
[
"Superman",
Expand Down Expand Up @@ -62,43 +63,28 @@ public class ApplicationDbContextInitializer(
"X-Men"
];

private const int NumHeroes = 20;

private const int NumTeams = 5;

public async Task InitializeAsync()
public ApplicationDbContextInitializer(ApplicationDbContext DbContext) : base(DbContext)
{
try
{
if (dbContext.Database.IsSqlServer())
{
await dbContext.Database.MigrateAsync();
}
}
catch (Exception e)
{
logger.LogError(e, "An error occurred while migrating or initializing the database");
throw;
}
}

public async Task SeedAsync()
public async Task SeedDataAsync(CancellationToken cancellationToken)
{
try
var strategy = DbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
// Seed the database
await using var transaction = await DbContext.Database.BeginTransactionAsync(cancellationToken);
var heroes = await SeedHeroes();
await SeedTeams(heroes);
}
catch (Exception e)
{
logger.LogError(e, "An error occurred while seeding the database");
throw;
}
// await DbContext.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
});
}

private async Task<List<Hero>> SeedHeroes()
{
if (dbContext.Heroes.Any())
if (DbContext.Heroes.Any())
return [];

var faker = new Faker<Hero>()
Expand All @@ -113,15 +99,15 @@ private async Task<List<Hero>> SeedHeroes()
});

var heroes = faker.Generate(NumHeroes);
await dbContext.Heroes.AddRangeAsync(heroes);
await dbContext.SaveChangesAsync();
await DbContext.Heroes.AddRangeAsync(heroes);
await DbContext.SaveChangesAsync();

return heroes;
}

private async Task SeedTeams(List<Hero> heroes)
{
if (dbContext.Teams.Any())
if (DbContext.Teams.Any())
return;

var faker = new Faker<Team>()
Expand All @@ -146,7 +132,7 @@ private async Task SeedTeams(List<Hero> heroes)
});

var teams = faker.Generate(NumTeams);
await dbContext.Teams.AddRangeAsync(teams);
await dbContext.SaveChangesAsync();
await DbContext.Teams.AddRangeAsync(teams);
await DbContext.SaveChangesAsync();
}
}
44 changes: 44 additions & 0 deletions tools/MigrationService/Initializers/DbContextInitializerBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;

namespace MigrationService.Initializers;

public abstract class DbContextInitializerBase<T> where T : DbContext
{
protected readonly T DbContext;

// public constructor needed for DI
internal DbContextInitializerBase(T dbContext)
{
DbContext = dbContext;
}

public async Task EnsureDatabaseAsync(CancellationToken cancellationToken)
{
var dbCreator = DbContext.GetService<IRelationalDatabaseCreator>();

var strategy = DbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
// Create the database if it does not exist.
// Do this first so there is then a database to start a transaction against.
if (!await dbCreator.ExistsAsync(cancellationToken))
{
await dbCreator.CreateAsync(cancellationToken);
}
});
}

public async Task RunMigrationAsync(CancellationToken cancellationToken)
{
var strategy = DbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
// Run migration in a transaction to avoid partial migration if it fails.
// await using var transaction = await DbContext.Database.BeginTransactionAsync(cancellationToken);
await DbContext.Database.MigrateAsync(cancellationToken);
// await transaction.CommitAsync(cancellationToken);
});
}
}
13 changes: 13 additions & 0 deletions tools/MigrationService/MigrationService.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<ItemGroup>
<PackageReference Include="Aspire.Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0-rc.1.24511.1" />
<PackageReference Include="Bogus" Version="35.6.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0-rc.2.24473.5" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Application\Application.csproj" />
<ProjectReference Include="..\..\src\Infrastructure\Infrastructure.csproj" />
</ItemGroup>
</Project>
Loading

0 comments on commit 986c62e

Please sign in to comment.