Skip to content

Commit

Permalink
πŸ‘·β€β™‚οΈ 306 move seed data and migrations out of api (#309)
Browse files Browse the repository at this point in the history
* Initial version of up.ps1

* Updated up.ps1

* Fixes from code review

* Update README.md

* Fixed integration tests

* Removed unneeded code

* Added missing call to interceptor

* Removed unneeded code
  • Loading branch information
danielmackay authored May 21, 2024
1 parent 2a42120 commit 91d1b69
Show file tree
Hide file tree
Showing 15 changed files with 160 additions and 40 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,16 @@ dotnet new ssw-ca-command --name {{CommandName}} --entityName {{Entity}} --slnNa

### Running the Solution

1. Start dockerized SQL Server
1. Start dockerized SQL Server, create and seed the database.

Windows:
```ps
.\up.ps1
```

Mac/Linux:
```bash
docker compose up
pwsh ./up.ps1
```

2. Run the solution
Expand Down
10 changes: 10 additions & 0 deletions SSW.CleanArchitecture.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
docker-compose.yml = docker-compose.yml
global.json = global.json
README.md = README.md
up.ps1 = up.ps1
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{A388D69B-2789-4D2E-A6D9-331D8571DA7E}"
Expand All @@ -34,6 +35,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain.UnitTests", "tests\D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.IntegrationTests", "tests\WebApi.IntegrationTests\WebApi.IntegrationTests.csproj", "{6FAECF07-C024-45F6-B78A-A2771833F867}"
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -68,6 +73,10 @@ 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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -80,6 +89,7 @@ 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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8975F724-46D1-4802-8C4B-6B386B69846F}
Expand Down
11 changes: 8 additions & 3 deletions src/Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
services.AddScoped<DispatchDomainEventsInterceptor>();

services.AddDbContext<IApplicationDbContext, ApplicationDbContext>(options =>
{
options.AddInterceptors(
services.BuildServiceProvider().GetRequiredService<EntitySaveChangesInterceptor>(),
services.BuildServiceProvider().GetRequiredService<DispatchDomainEventsInterceptor>()
);
options.UseSqlServer(config.GetConnectionString("DefaultConnection"), builder =>
{
builder.MigrationsAssembly(typeof(DependencyInjection).Assembly.FullName);
builder.EnableRetryOnFailure();
}));

services.AddScoped<ApplicationDbContextInitializer>();
});
});

services.AddSingleton(TimeProvider.System);

Expand Down
3 changes: 1 addition & 2 deletions src/Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Bogus" Version="34.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
</ItemGroup>
</Project>
</Project>
10 changes: 1 addition & 9 deletions src/Infrastructure/Persistence/ApplicationDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
namespace SSW.CleanArchitecture.Infrastructure.Persistence;

public class ApplicationDbContext(
DbContextOptions options,
EntitySaveChangesInterceptor saveChangesInterceptor,
DispatchDomainEventsInterceptor dispatchDomainEventsInterceptor)
DbContextOptions options)
: DbContext(options), IApplicationDbContext
{
public DbSet<TodoItem> TodoItems => Set<TodoItem>();
Expand All @@ -29,11 +27,5 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
base.OnModelCreating(modelBuilder);
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Order of the interceptors is important
optionsBuilder.AddInterceptors(saveChangesInterceptor, dispatchDomainEventsInterceptor);
}

private DbSet<T> AggregateRootSet<T>() where T : class, IAggregateRoot => Set<T>();
}
8 changes: 1 addition & 7 deletions src/WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();

// Initialise and seed database
using var scope = app.Services.CreateScope();
var initializer = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitializer>();
await initializer.InitializeAsync();
await initializer.SeedAsync();
}
else
{
Expand All @@ -49,4 +43,4 @@

app.Run();

public partial class Program { }
public partial class Program { }
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SSW.CleanArchitecture.Database;
using SSW.CleanArchitecture.Infrastructure.Persistence;

namespace WebApi.IntegrationTests.Common.Fixtures;
Expand Down Expand Up @@ -33,6 +34,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
});

// Override default DB registration to use out Test Container instead
builder.ConfigureTestServices(services => services.ReplaceDbContext<ApplicationDbContext>(Database));
builder.ConfigureTestServices(services =>
{
services.ReplaceDbContext<ApplicationDbContext>(Database);
services.AddScoped<ApplicationDbContextInitializer>();
});
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
ο»Ώusing Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using SSW.CleanArchitecture.Infrastructure.Persistence.Interceptors;

namespace WebApi.IntegrationTests.Common.Fixtures;

Expand All @@ -9,15 +11,22 @@ internal static class ServiceCollectionExt
/// <summary>
/// Replaces the DbContext with a new instance using the provided database container
/// </summary>
internal static IServiceCollection ReplaceDbContext<T>(this IServiceCollection services,
internal static IServiceCollection ReplaceDbContext<T>(
this IServiceCollection services,
DatabaseContainer databaseContainer) where T : DbContext
{
services
.RemoveAll<DbContextOptions<T>>()
.RemoveAll<T>()
.AddDbContext<T>((_, options) =>
{
options.UseSqlServer(databaseContainer.ConnectionString,
b => b.MigrationsAssembly(typeof(T).Assembly.FullName)));
b => b.MigrationsAssembly(typeof(T).Assembly.FullName));
options.AddInterceptors(
services.BuildServiceProvider().GetRequiredService<EntitySaveChangesInterceptor>()
);
});

return services;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Respawn;
using SSW.CleanArchitecture.Database;

namespace WebApi.IntegrationTests.Common.Fixtures;

Expand All @@ -19,9 +20,16 @@ public class TestingDatabaseFixture : IAsyncLifetime

public async Task InitializeAsync()
{
// Initialize DB Container
await Factory.Database.InitializeAsync();
ScopeFactory = Factory.Services.GetRequiredService<IServiceScopeFactory>();

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

// 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 @@ -34,6 +34,7 @@

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

</Project>
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
ο»Ώusing Microsoft.EntityFrameworkCore;
ο»Ώusing Bogus;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SSW.CleanArchitecture.Domain.TodoItems;
using Bogus;
using SSW.CleanArchitecture.Domain.Heroes;
using SSW.CleanArchitecture.Domain.Teams;
using SSW.CleanArchitecture.Infrastructure.Persistence;

namespace SSW.CleanArchitecture.Infrastructure.Persistence;
namespace SSW.CleanArchitecture.Database;

public class ApplicationDbContextInitializer(
ILogger<ApplicationDbContextInitializer> logger,
Expand Down Expand Up @@ -43,15 +43,6 @@ public class ApplicationDbContextInitializer(
"Intelligence"
];

private readonly string[] _teamNames =
[
"Marvel",
"Avengers",
"DC",
"Justice League",
"X-Men"
];

private readonly string[] _missionNames =
[
"Save the world",
Expand All @@ -61,7 +52,17 @@ public class ApplicationDbContextInitializer(
"Protect the city"
];

private readonly string[] _teamNames =
[
"Marvel",
"Avengers",
"DC",
"Justice League",
"X-Men"
];

private const int NumHeroes = 20;

private const int NumTeams = 5;

public async Task InitializeAsync()
Expand Down Expand Up @@ -94,7 +95,6 @@ public async Task SeedAsync()
}
}


private async Task<List<Hero>> SeedHeroes()
{
if (dbContext.Heroes.Any())
Expand Down
28 changes: 28 additions & 0 deletions tools/Database/Database.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>SSW.CleanArchitecture.Database</RootNamespace>
<AssemblyName>SSW.CleanArchitecture.Database</AssemblyName>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

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

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Bogus" Version="34.0.2" />
</ItemGroup>

<ItemGroup>
<None Remove="appsettings.json" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
36 changes: 36 additions & 0 deletions tools/Database/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
ο»Ώusing Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SSW.CleanArchitecture.Application.Common.Interfaces;
using SSW.CleanArchitecture.Database;
using SSW.CleanArchitecture.Infrastructure;
using SSW.CleanArchitecture.Infrastructure.Persistence;
using SSW.CleanArchitecture.Infrastructure.Persistence.Interceptors;

var builder = Host.CreateDefaultBuilder(args);

builder.ConfigureServices((context, services) =>
{
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(context.Configuration.GetConnectionString("DefaultConnection"), opt =>
{
opt.MigrationsAssembly(typeof(DependencyInjection).Assembly.FullName);
});
options.AddInterceptors(services.BuildServiceProvider().GetRequiredService<EntitySaveChangesInterceptor>());
});
services.AddScoped<ApplicationDbContextInitializer>();
});

var app = builder.Build();
app.Start();

// Initialise and seed database
using var scope = app.Services.CreateScope();
var initializer = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitializer>();
await initializer.InitializeAsync();
await initializer.SeedAsync();
11 changes: 11 additions & 0 deletions tools/Database/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=localhost,1433;Initial Catalog=CleanArchitecture;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;MultipleActiveResultSets=True;TrustServerCertificate=True;Connection Timeout=30;"
}
}
16 changes: 16 additions & 0 deletions up.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Param(
[switch]$skipDeploy = $false
)

Write-Host "🚒 Starting Docker Compose" -ForegroundColor Green
docker compose up -d

if (-not $skipDeploy) {
$upScriptPath = $Script:MyInvocation.MyCommand.Path | Split-Path

Write-Host "πŸš€ Creating and Seeding Database" -ForegroundColor Green
Set-Location ./tools/Database/
dotnet run

Set-Location $upScriptPath
}

0 comments on commit 91d1b69

Please sign in to comment.