mirror of
https://github.com/collinbarrett/FilterLists.git
synced 2026-03-11 09:04:27 +00:00
refactor(dir): ♻️ apply migrations from API
This commit is contained in:
parent
21257bf8f6
commit
d668655af1
16 changed files with 93 additions and 160 deletions
20
.github/README.md
vendored
20
.github/README.md
vendored
|
|
@ -92,38 +92,38 @@ dotnet ef migrations add <MigrationName> --project FilterLists.Directory.Infrast
|
|||
|
||||
Create an instance of SQL Server containing the users below.
|
||||
|
||||
##### DirectoryMigrations user for applying EF Core migrations
|
||||
##### Migrations user for applying EF Core migrations
|
||||
|
||||
```sql
|
||||
USE [master];
|
||||
GO
|
||||
|
||||
CREATE LOGIN [DirectoryMigrations] WITH PASSWORD = 'my_password';
|
||||
CREATE LOGIN [Migrations] WITH PASSWORD = 'my_password';
|
||||
GO
|
||||
|
||||
USE [directorydb];
|
||||
GO
|
||||
|
||||
CREATE USER [DirectoryMigrations] FOR LOGIN [DirectoryMigrations];
|
||||
ALTER ROLE [db_ddladmin] ADD MEMBER [DirectoryMigrations]; -- to apply migrations
|
||||
ALTER ROLE [db_datareader] ADD MEMBER [DirectoryMigrations]; -- to read from __EFMigrationsHistory
|
||||
ALTER ROLE [db_datawriter] ADD MEMBER [DirectoryMigrations]; -- to insert to __EFMigrationsHistory
|
||||
CREATE USER [Migrations] FOR LOGIN [Migrations];
|
||||
ALTER ROLE [db_ddladmin] ADD MEMBER [Migrations]; -- to apply migrations
|
||||
ALTER ROLE [db_datareader] ADD MEMBER [Migrations]; -- to read from __EFMigrationsHistory
|
||||
ALTER ROLE [db_datawriter] ADD MEMBER [Migrations]; -- to insert to __EFMigrationsHistory
|
||||
```
|
||||
|
||||
##### DirectoryApiReadonly for API runtime reads
|
||||
##### ApiReadonly for API runtime reads
|
||||
|
||||
```sql
|
||||
USE [master];
|
||||
GO
|
||||
|
||||
CREATE LOGIN [DirectoryApiReadonly] WITH PASSWORD = 'my_password';
|
||||
CREATE LOGIN [ApiReadonly] WITH PASSWORD = 'my_password';
|
||||
GO
|
||||
|
||||
USE [directorydb];
|
||||
GO
|
||||
|
||||
CREATE USER [DirectoryApiReadonly] FOR LOGIN [DirectoryApiReadonly];
|
||||
ALTER ROLE [db_datareader] ADD MEMBER [DirectoryApiReadonly];
|
||||
CREATE USER [ApiReadonly] FOR LOGIN [ApiReadonly];
|
||||
ALTER ROLE [db_datareader] ADD MEMBER [ApiReadonly];
|
||||
```
|
||||
|
||||
# Acknowledgements
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FilterLists.Directory.Infrastructure.Migrations\FilterLists.Directory.Infrastructure.Migrations.csproj"/>
|
||||
<ProjectReference Include="..\FilterLists.Directory.Infrastructure\FilterLists.Directory.Infrastructure.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AnalysisMode>Recommended</AnalysisMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\FilterLists.ServiceDefaults\FilterLists.ServiceDefaults.csproj"/>
|
||||
<ProjectReference Include="..\FilterLists.Directory.Infrastructure.Migrations\FilterLists.Directory.Infrastructure.Migrations.csproj"/>
|
||||
<ProjectReference Include="..\FilterLists.Directory.Infrastructure\FilterLists.Directory.Infrastructure.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
using FilterLists.Directory.Infrastructure;
|
||||
using FilterLists.Directory.Infrastructure.MigrationService;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.AddInfrastructure();
|
||||
builder.Services.AddHostedService<Worker>();
|
||||
|
||||
var host = builder.Build();
|
||||
host.Run();
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"FilterLists.Directory.MigrationService": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace FilterLists.Directory.Infrastructure.MigrationService;
|
||||
|
||||
/// <remarks>https://learn.microsoft.com/en-us/dotnet/aspire/database/ef-core-migrations#create-the-migration-service</remarks>
|
||||
public sealed class Worker(
|
||||
IServiceProvider serviceProvider,
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
IHostEnvironment hostEnvironment) : BackgroundService
|
||||
{
|
||||
private const string ActivitySourceName = "Migrations";
|
||||
private static readonly ActivitySource ActivitySource = new(ActivitySourceName);
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// allow SQL Server container time to start
|
||||
await Task.Delay(3000, stoppingToken);
|
||||
|
||||
// ReSharper disable once ExplicitCallerInfoArgument
|
||||
using var activity = ActivitySource.StartActivity("Migrating database", ActivityKind.Client);
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<QueryDbContext>();
|
||||
|
||||
if (hostEnvironment.IsDevelopment()) await EnsureDatabaseAsync(dbContext, stoppingToken);
|
||||
await RunMigrationAsync(dbContext, stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity?.RecordException(ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
hostApplicationLifetime.StopApplication();
|
||||
}
|
||||
|
||||
private static async Task EnsureDatabaseAsync(QueryDbContext dbContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var dbCreator = dbContext.GetService<IRelationalDatabaseCreator>();
|
||||
|
||||
var strategy = dbContext.Database.CreateExecutionStrategy();
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
if (!await dbCreator.ExistsAsync(cancellationToken)) await dbCreator.CreateAsync(cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task RunMigrationAsync(QueryDbContext dbContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var strategy = dbContext.Database.CreateExecutionStrategy();
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,26 @@
|
|||
using FilterLists.Directory.Infrastructure.Persistence.Queries;
|
||||
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace FilterLists.Directory.Infrastructure;
|
||||
|
||||
public static class ConfigurationExtensions
|
||||
{
|
||||
private const string MigrationsAssembly = "FilterLists.Directory.Infrastructure.Migrations";
|
||||
|
||||
public static void AddInfrastructure(this IHostApplicationBuilder builder)
|
||||
{
|
||||
// TODO: use different connection strings for migrations and queries (https://stackoverflow.com/q/78564037/2343739)
|
||||
builder.AddSqlServerDbContext<QueryDbContext>("directorydb",
|
||||
_ => { },
|
||||
o => o.UseSqlServer(so => so.MigrationsAssembly("FilterLists.Directory.Infrastructure.Migrations")));
|
||||
o => o.UseSqlServer(so =>
|
||||
// retry on Microsoft.Data.SqlClient.SqlException (0x80131904): A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: TCP Provider, error: 0 - Undefined error: 0)
|
||||
so.EnableRetryOnFailure([0])
|
||||
.MigrationsAssembly(MigrationsAssembly))
|
||||
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
|
||||
|
||||
builder.Services.AddHostedService<MigrationService>();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,10 @@
|
|||
|
||||
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
|
||||
|
||||
public sealed class QueryDbContext : DbContext
|
||||
public sealed class QueryDbContext(DbContextOptions<QueryDbContext> options) : DbContext(options)
|
||||
{
|
||||
private const string ReadOnlyErrorMessage = "This context is read-only and cannot save changes.";
|
||||
|
||||
public QueryDbContext(DbContextOptions<QueryDbContext> options) : base(options)
|
||||
{
|
||||
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
}
|
||||
|
||||
public IQueryable<FilterList> FilterLists => Set<FilterList>();
|
||||
public IQueryable<Language> Languages => Set<Language>();
|
||||
public IQueryable<License> Licenses => Set<License>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
using System.Diagnostics;
|
||||
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace FilterLists.Directory.Infrastructure.Persistence.Queries;
|
||||
|
||||
public sealed class MigrationService(IServiceProvider serviceProvider) : IHostedLifecycleService
|
||||
{
|
||||
private static readonly ActivitySource ActivitySource = new(nameof(MigrationService));
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StartingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity();
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
await using var dbContext = scope.ServiceProvider.GetRequiredService<QueryDbContext>();
|
||||
var dbCreator = dbContext.GetService<IRelationalDatabaseCreator>();
|
||||
var strategy = dbContext.Database.CreateExecutionStrategy();
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
if (!await dbCreator.ExistsAsync(cancellationToken)) await dbCreator.CreateAsync(cancellationToken);
|
||||
});
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity?.RecordException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StoppingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StoppedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace FilterLists.Directory.Infrastructure.Persistence;
|
||||
namespace FilterLists.Directory.Infrastructure.Persistence.Queries;
|
||||
|
||||
internal static class SeedConfigurationExtensions
|
||||
{
|
||||
|
|
@ -12,7 +12,6 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Directory\FilterLists.Directory.Api\FilterLists.Directory.Api.csproj"/>
|
||||
<ProjectReference Include="..\Directory\FilterLists.Directory.Infrastructure.MigrationService\FilterLists.Directory.Infrastructure.MigrationService.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -4,18 +4,10 @@
|
|||
|
||||
var appInsights = builder.AddAzureApplicationInsights("appinsights");
|
||||
|
||||
// TODO: use separate users (db_ddladmin only for migrations) https://stackoverflow.com/q/78564037/2343739
|
||||
var directoryDb = builder.AddSqlServer("directorysqlserver")
|
||||
.PublishAsConnectionString() // customized Azure resource for free tier, don't yet trust Aspire to provision db
|
||||
.WithDataVolume() // don't re-seed db on every startup locally, requires https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/persist-data-volumes#create-a-persistent-password
|
||||
.WithDataVolume()
|
||||
.AddDatabase("directorydb");
|
||||
|
||||
// TODO: migrate published db from run-once instance (Container Apps 'App" type restarts)
|
||||
if (builder.ExecutionContext.IsRunMode)
|
||||
builder.AddProject<FilterLists_Directory_Infrastructure_MigrationService>("directorymigrationservice")
|
||||
.WithReference(directoryDb)
|
||||
.WithReference(appInsights);
|
||||
|
||||
builder.AddProject<FilterLists_Directory_Api>("directoryapi")
|
||||
.WithReference(directoryDb)
|
||||
.WithReference(appInsights)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilterLists.Tests", "Filter
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Directory", "Directory", "{C4F6197D-9546-480E-AE92-1FC264D8E88F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FilterLists.Directory.Infrastructure.MigrationService", "Directory\FilterLists.Directory.Infrastructure.MigrationService\FilterLists.Directory.Infrastructure.MigrationService.csproj", "{5DD434A8-D394-4178-9DF3-669EC85EA229}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FilterLists.Directory.Infrastructure.Migrations", "Directory\FilterLists.Directory.Infrastructure.Migrations\FilterLists.Directory.Infrastructure.Migrations.csproj", "{37170729-4BC9-412F-9ADF-97592A677E90}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FilterLists.Directory.Infrastructure", "Directory\FilterLists.Directory.Infrastructure\FilterLists.Directory.Infrastructure.csproj", "{02B4FF36-8012-42CD-BE3E-92475C091A4C}"
|
||||
|
|
@ -42,10 +40,6 @@ Global
|
|||
{063F0781-ECBC-4254-AF2F-1B75C6CC03B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{063F0781-ECBC-4254-AF2F-1B75C6CC03B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{063F0781-ECBC-4254-AF2F-1B75C6CC03B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5DD434A8-D394-4178-9DF3-669EC85EA229}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5DD434A8-D394-4178-9DF3-669EC85EA229}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5DD434A8-D394-4178-9DF3-669EC85EA229}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5DD434A8-D394-4178-9DF3-669EC85EA229}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{37170729-4BC9-412F-9ADF-97592A677E90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{37170729-4BC9-412F-9ADF-97592A677E90}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{37170729-4BC9-412F-9ADF-97592A677E90}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
|
@ -67,7 +61,6 @@ Global
|
|||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{85A14D07-5C37-458A-B74B-4EE67BBA7102} = {C4F6197D-9546-480E-AE92-1FC264D8E88F}
|
||||
{5DD434A8-D394-4178-9DF3-669EC85EA229} = {C4F6197D-9546-480E-AE92-1FC264D8E88F}
|
||||
{37170729-4BC9-412F-9ADF-97592A677E90} = {C4F6197D-9546-480E-AE92-1FC264D8E88F}
|
||||
{02B4FF36-8012-42CD-BE3E-92475C091A4C} = {C4F6197D-9546-480E-AE92-1FC264D8E88F}
|
||||
{0D4420C3-283B-4C0A-88B4-D39E2033B80F} = {C4F6197D-9546-480E-AE92-1FC264D8E88F}
|
||||
|
|
|
|||
Loading…
Reference in a new issue