From 5ab8cf8dcc7ef4053b4b70795530b33c657b3c9b Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Sat, 19 Sep 2026 10:50:21 +0100 Subject: [PATCH 1/2] Use EF Core migrations for SQL Server test databases --- .../HealthMonitoringMigrationsTests.cs | 19 ++ .../Persistence/RepositoryTests.cs | 2 +- .../20260919092909_InitialCreate.Designer.cs | 320 ++++++++++++++++++ .../20260919092909_InitialCreate.cs | 205 +++++++++++ .../HealthMonitoringDbContextModelSnapshot.cs | 317 +++++++++++++++++ .../HealthMonitoringSchemaInitializer.cs | 221 ------------ HealthMonitoring/Program.cs | 6 +- README.md | 4 +- 8 files changed, 868 insertions(+), 226 deletions(-) create mode 100644 HealthMonitoring.Tests/Persistence/HealthMonitoringMigrationsTests.cs create mode 100644 HealthMonitoring/Migrations/20260919092909_InitialCreate.Designer.cs create mode 100644 HealthMonitoring/Migrations/20260919092909_InitialCreate.cs create mode 100644 HealthMonitoring/Migrations/HealthMonitoringDbContextModelSnapshot.cs delete mode 100644 HealthMonitoring/Persistence/HealthMonitoringSchemaInitializer.cs diff --git a/HealthMonitoring.Tests/Persistence/HealthMonitoringMigrationsTests.cs b/HealthMonitoring.Tests/Persistence/HealthMonitoringMigrationsTests.cs new file mode 100644 index 0000000..7cb8067 --- /dev/null +++ b/HealthMonitoring.Tests/Persistence/HealthMonitoringMigrationsTests.cs @@ -0,0 +1,19 @@ +using HealthMonitoring.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace HealthMonitoring.Tests.Persistence; + +public sealed class HealthMonitoringMigrationsTests +{ + [Fact] + public void Health_monitoring_context_has_an_EF_migration() + { + var options = new DbContextOptionsBuilder() + .UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=HealthMonitoringTest;") + .Options; + + using var context = new HealthMonitoringDbContext(options); + + Assert.NotEmpty(context.Database.GetMigrations()); + } +} diff --git a/HealthMonitoring.Tests/Persistence/RepositoryTests.cs b/HealthMonitoring.Tests/Persistence/RepositoryTests.cs index 3640290..c0f0b85 100644 --- a/HealthMonitoring.Tests/Persistence/RepositoryTests.cs +++ b/HealthMonitoring.Tests/Persistence/RepositoryTests.cs @@ -89,7 +89,7 @@ private SqlServerTestDatabase(HealthMonitoringDbContext context) .UseSqlServer(connectionString) .Options; var context = new HealthMonitoringDbContext(options); - await context.Database.EnsureCreatedAsync(); + await context.Database.MigrateAsync(); return new SqlServerTestDatabase(context); } diff --git a/HealthMonitoring/Migrations/20260919092909_InitialCreate.Designer.cs b/HealthMonitoring/Migrations/20260919092909_InitialCreate.Designer.cs new file mode 100644 index 0000000..81831d1 --- /dev/null +++ b/HealthMonitoring/Migrations/20260919092909_InitialCreate.Designer.cs @@ -0,0 +1,320 @@ +// +using System; +using HealthMonitoring.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace HealthMonitoring.Migrations +{ + [DbContext(typeof(HealthMonitoringDbContext))] + [Migration("20260919092909_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("HealthMonitoring.Domain.HealthCheckResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DataJson") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Duration") + .HasColumnType("bigint"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("HealthObservationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("HealthObservationId"); + + b.ToTable("HealthCheckResults"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.HealthObservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AggregateStatus") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Error") + .HasColumnType("nvarchar(max)"); + + b.Property("HttpStatusCode") + .HasColumnType("int"); + + b.Property("MonitoredServiceId") + .HasColumnType("uniqueidentifier"); + + b.Property("ObservedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("RawResponseJson") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseDuration") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("MonitoredServiceId", "ObservedAtUtc"); + + b.ToTable("HealthObservations"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.MonitoredService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ArchivedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("ConnectionString") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Environment") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Group") + .HasColumnType("nvarchar(max)"); + + b.Property("HealthUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("Host") + .HasColumnType("nvarchar(max)"); + + b.Property("IgnoreCertificateErrors") + .HasColumnType("bit"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("LastRegisteredAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("MonitorType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("PollingInterval") + .HasColumnType("bigint"); + + b.Property("RequestTimeout") + .HasColumnType("bigint"); + + b.Property("RetentionPeriod") + .HasColumnType("bigint"); + + b.Property("ServiceId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StatusPolicyJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId") + .IsUnique(); + + b.ToTable("MonitoredServices"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.ServiceDependencyLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("DependencyName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("MonitoredServiceId") + .HasColumnType("uniqueidentifier"); + + b.Property("TargetMonitoredServiceId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("TargetMonitoredServiceId"); + + b.HasIndex("MonitoredServiceId", "DependencyName") + .IsUnique(); + + b.ToTable("ServiceDependencyLinks"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.ServiceIncident", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EndedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("FirstObservationId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastObservationId") + .HasColumnType("uniqueidentifier"); + + b.Property("MonitoredServiceId") + .HasColumnType("uniqueidentifier"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("StartedAtUtc") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("MonitoredServiceId", "StartedAtUtc"); + + b.ToTable("ServiceIncidents"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.ServiceStatusSnapshot", b => + { + b.Property("MonitoredServiceId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentIncidentId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastError") + .HasColumnType("nvarchar(max)"); + + b.Property("LastObservationId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastObservedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("LastResponseDuration") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("MonitoredServiceId"); + + b.ToTable("ServiceStatusSnapshots"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.HealthCheckResult", b => + { + b.HasOne("HealthMonitoring.Domain.HealthObservation", null) + .WithMany("Checks") + .HasForeignKey("HealthObservationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.ServiceDependencyLink", b => + { + b.HasOne("HealthMonitoring.Domain.MonitoredService", null) + .WithMany() + .HasForeignKey("MonitoredServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("HealthMonitoring.Domain.MonitoredService", null) + .WithMany() + .HasForeignKey("TargetMonitoredServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.HealthObservation", b => + { + b.Navigation("Checks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HealthMonitoring/Migrations/20260919092909_InitialCreate.cs b/HealthMonitoring/Migrations/20260919092909_InitialCreate.cs new file mode 100644 index 0000000..2c0073d --- /dev/null +++ b/HealthMonitoring/Migrations/20260919092909_InitialCreate.cs @@ -0,0 +1,205 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace HealthMonitoring.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "HealthObservations", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MonitoredServiceId = table.Column(type: "uniqueidentifier", nullable: false), + ObservedAtUtc = table.Column(type: "datetimeoffset", nullable: false), + Status = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + AggregateStatus = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + HttpStatusCode = table.Column(type: "int", nullable: true), + ResponseDuration = table.Column(type: "bigint", nullable: false), + RawResponseJson = table.Column(type: "nvarchar(max)", nullable: true), + Error = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HealthObservations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "MonitoredServices", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ServiceId = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Name = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: false), + Environment = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + MonitorType = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Group = table.Column(type: "nvarchar(max)", nullable: true), + Description = table.Column(type: "nvarchar(max)", nullable: true), + HealthUrl = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: false), + ConnectionString = table.Column(type: "nvarchar(max)", nullable: true), + IgnoreCertificateErrors = table.Column(type: "bit", nullable: false), + IsEnabled = table.Column(type: "bit", nullable: false), + PollingInterval = table.Column(type: "bigint", nullable: false), + RequestTimeout = table.Column(type: "bigint", nullable: false), + RetentionPeriod = table.Column(type: "bigint", nullable: false), + StatusPolicyJson = table.Column(type: "nvarchar(max)", nullable: false), + Version = table.Column(type: "nvarchar(max)", nullable: true), + Host = table.Column(type: "nvarchar(max)", nullable: true), + LastRegisteredAtUtc = table.Column(type: "datetimeoffset", nullable: true), + CreatedAtUtc = table.Column(type: "datetimeoffset", nullable: false), + UpdatedAtUtc = table.Column(type: "datetimeoffset", nullable: false), + ArchivedAtUtc = table.Column(type: "datetimeoffset", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MonitoredServices", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServiceIncidents", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MonitoredServiceId = table.Column(type: "uniqueidentifier", nullable: false), + StartedAtUtc = table.Column(type: "datetimeoffset", nullable: false), + EndedAtUtc = table.Column(type: "datetimeoffset", nullable: true), + Severity = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + FirstObservationId = table.Column(type: "uniqueidentifier", nullable: false), + LastObservationId = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServiceIncidents", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServiceStatusSnapshots", + columns: table => new + { + MonitoredServiceId = table.Column(type: "uniqueidentifier", nullable: false), + Status = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + LastObservedAtUtc = table.Column(type: "datetimeoffset", nullable: true), + LastResponseDuration = table.Column(type: "bigint", nullable: true), + CurrentIncidentId = table.Column(type: "uniqueidentifier", nullable: true), + LastError = table.Column(type: "nvarchar(max)", nullable: true), + LastObservationId = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ServiceStatusSnapshots", x => x.MonitoredServiceId); + }); + + migrationBuilder.CreateTable( + name: "HealthCheckResults", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + HealthObservationId = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: false), + Status = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: true), + Duration = table.Column(type: "bigint", nullable: false), + Exception = table.Column(type: "nvarchar(max)", nullable: true), + DataJson = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HealthCheckResults", x => x.Id); + table.ForeignKey( + name: "FK_HealthCheckResults_HealthObservations_HealthObservationId", + column: x => x.HealthObservationId, + principalTable: "HealthObservations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ServiceDependencyLinks", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MonitoredServiceId = table.Column(type: "uniqueidentifier", nullable: false), + DependencyName = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: false), + TargetMonitoredServiceId = table.Column(type: "uniqueidentifier", nullable: false), + CreatedAtUtc = table.Column(type: "datetimeoffset", nullable: false), + UpdatedAtUtc = table.Column(type: "datetimeoffset", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServiceDependencyLinks", x => x.Id); + table.ForeignKey( + name: "FK_ServiceDependencyLinks_MonitoredServices_MonitoredServiceId", + column: x => x.MonitoredServiceId, + principalTable: "MonitoredServices", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ServiceDependencyLinks_MonitoredServices_TargetMonitoredServiceId", + column: x => x.TargetMonitoredServiceId, + principalTable: "MonitoredServices", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_HealthCheckResults_HealthObservationId", + table: "HealthCheckResults", + column: "HealthObservationId"); + + migrationBuilder.CreateIndex( + name: "IX_HealthObservations_MonitoredServiceId_ObservedAtUtc", + table: "HealthObservations", + columns: new[] { "MonitoredServiceId", "ObservedAtUtc" }); + + migrationBuilder.CreateIndex( + name: "IX_MonitoredServices_ServiceId", + table: "MonitoredServices", + column: "ServiceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDependencyLinks_MonitoredServiceId_DependencyName", + table: "ServiceDependencyLinks", + columns: new[] { "MonitoredServiceId", "DependencyName" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDependencyLinks_TargetMonitoredServiceId", + table: "ServiceDependencyLinks", + column: "TargetMonitoredServiceId"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceIncidents_MonitoredServiceId_StartedAtUtc", + table: "ServiceIncidents", + columns: new[] { "MonitoredServiceId", "StartedAtUtc" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "HealthCheckResults"); + + migrationBuilder.DropTable( + name: "ServiceDependencyLinks"); + + migrationBuilder.DropTable( + name: "ServiceIncidents"); + + migrationBuilder.DropTable( + name: "ServiceStatusSnapshots"); + + migrationBuilder.DropTable( + name: "HealthObservations"); + + migrationBuilder.DropTable( + name: "MonitoredServices"); + } + } +} diff --git a/HealthMonitoring/Migrations/HealthMonitoringDbContextModelSnapshot.cs b/HealthMonitoring/Migrations/HealthMonitoringDbContextModelSnapshot.cs new file mode 100644 index 0000000..07c1a6f --- /dev/null +++ b/HealthMonitoring/Migrations/HealthMonitoringDbContextModelSnapshot.cs @@ -0,0 +1,317 @@ +// +using System; +using HealthMonitoring.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace HealthMonitoring.Migrations +{ + [DbContext(typeof(HealthMonitoringDbContext))] + partial class HealthMonitoringDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("HealthMonitoring.Domain.HealthCheckResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DataJson") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Duration") + .HasColumnType("bigint"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("HealthObservationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("HealthObservationId"); + + b.ToTable("HealthCheckResults"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.HealthObservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AggregateStatus") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Error") + .HasColumnType("nvarchar(max)"); + + b.Property("HttpStatusCode") + .HasColumnType("int"); + + b.Property("MonitoredServiceId") + .HasColumnType("uniqueidentifier"); + + b.Property("ObservedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("RawResponseJson") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseDuration") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("MonitoredServiceId", "ObservedAtUtc"); + + b.ToTable("HealthObservations"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.MonitoredService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ArchivedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("ConnectionString") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Environment") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Group") + .HasColumnType("nvarchar(max)"); + + b.Property("HealthUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("Host") + .HasColumnType("nvarchar(max)"); + + b.Property("IgnoreCertificateErrors") + .HasColumnType("bit"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("LastRegisteredAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("MonitorType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("PollingInterval") + .HasColumnType("bigint"); + + b.Property("RequestTimeout") + .HasColumnType("bigint"); + + b.Property("RetentionPeriod") + .HasColumnType("bigint"); + + b.Property("ServiceId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StatusPolicyJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId") + .IsUnique(); + + b.ToTable("MonitoredServices"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.ServiceDependencyLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("DependencyName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("MonitoredServiceId") + .HasColumnType("uniqueidentifier"); + + b.Property("TargetMonitoredServiceId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("TargetMonitoredServiceId"); + + b.HasIndex("MonitoredServiceId", "DependencyName") + .IsUnique(); + + b.ToTable("ServiceDependencyLinks"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.ServiceIncident", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EndedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("FirstObservationId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastObservationId") + .HasColumnType("uniqueidentifier"); + + b.Property("MonitoredServiceId") + .HasColumnType("uniqueidentifier"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("StartedAtUtc") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("MonitoredServiceId", "StartedAtUtc"); + + b.ToTable("ServiceIncidents"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.ServiceStatusSnapshot", b => + { + b.Property("MonitoredServiceId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentIncidentId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastError") + .HasColumnType("nvarchar(max)"); + + b.Property("LastObservationId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastObservedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("LastResponseDuration") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("MonitoredServiceId"); + + b.ToTable("ServiceStatusSnapshots"); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.HealthCheckResult", b => + { + b.HasOne("HealthMonitoring.Domain.HealthObservation", null) + .WithMany("Checks") + .HasForeignKey("HealthObservationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.ServiceDependencyLink", b => + { + b.HasOne("HealthMonitoring.Domain.MonitoredService", null) + .WithMany() + .HasForeignKey("MonitoredServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("HealthMonitoring.Domain.MonitoredService", null) + .WithMany() + .HasForeignKey("TargetMonitoredServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("HealthMonitoring.Domain.HealthObservation", b => + { + b.Navigation("Checks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HealthMonitoring/Persistence/HealthMonitoringSchemaInitializer.cs b/HealthMonitoring/Persistence/HealthMonitoringSchemaInitializer.cs deleted file mode 100644 index dcd0093..0000000 --- a/HealthMonitoring/Persistence/HealthMonitoringSchemaInitializer.cs +++ /dev/null @@ -1,221 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -#pragma warning disable EF1002 - -namespace HealthMonitoring.Persistence; - -public static class HealthMonitoringSchemaInitializer -{ - private static readonly LegacyDurationColumn[] LegacyDurationMigrations = - [ - LegacyDurationColumn.PollingInterval, - LegacyDurationColumn.RequestTimeout, - LegacyDurationColumn.RetentionPeriod, - LegacyDurationColumn.ResponseDuration, - LegacyDurationColumn.Duration, - LegacyDurationColumn.LastResponseDuration - ]; - - public static async Task InitializeAsync(HealthMonitoringDbContext dbContext, CancellationToken cancellationToken = default) - { - await dbContext.Database.EnsureCreatedAsync(cancellationToken); - - foreach (var migration in LegacyDurationMigrations) - await ConvertLegacyColumnAsync(dbContext, migration, cancellationToken); - - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[ServiceDependencyLinks]', N'U') IS NULL - BEGIN - CREATE TABLE [dbo].[ServiceDependencyLinks] - ( - [Id] uniqueidentifier NOT NULL, - [MonitoredServiceId] uniqueidentifier NOT NULL, - [DependencyName] nvarchar(300) NOT NULL, - [TargetMonitoredServiceId] uniqueidentifier NOT NULL, - [CreatedAtUtc] datetimeoffset(7) NOT NULL, - [UpdatedAtUtc] datetimeoffset(7) NOT NULL, - CONSTRAINT [PK_ServiceDependencyLinks] PRIMARY KEY ([Id]), - CONSTRAINT [FK_ServiceDependencyLinks_MonitoredServices_Parent] FOREIGN KEY ([MonitoredServiceId]) REFERENCES [dbo].[MonitoredServices] ([Id]), - CONSTRAINT [FK_ServiceDependencyLinks_MonitoredServices_Target] FOREIGN KEY ([TargetMonitoredServiceId]) REFERENCES [dbo].[MonitoredServices] ([Id]) - ); - CREATE UNIQUE INDEX [IX_ServiceDependencyLinks_MonitoredServiceId_DependencyName] - ON [dbo].[ServiceDependencyLinks] ([MonitoredServiceId], [DependencyName]); - END; - """, cancellationToken); - - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL - AND NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'MonitorType') - ALTER TABLE [dbo].[MonitoredServices] - ADD [MonitorType] nvarchar(50) NOT NULL CONSTRAINT [DF_MonitoredServices_MonitorType] DEFAULT (N'HttpHealthEndpoint'); - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL - AND NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'ConnectionString') - ALTER TABLE [dbo].[MonitoredServices] - ADD [ConnectionString] nvarchar(max) NULL; - """, cancellationToken); - - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL - AND NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'IgnoreCertificateErrors') - ALTER TABLE [dbo].[MonitoredServices] - ADD [IgnoreCertificateErrors] bit NOT NULL CONSTRAINT [DF_MonitoredServices_IgnoreCertificateErrors] DEFAULT (0); - """, cancellationToken); - } - - private static async Task ConvertLegacyColumnAsync( - HealthMonitoringDbContext dbContext, - LegacyDurationColumn migration, - CancellationToken cancellationToken) - { - switch (migration) - { - case LegacyDurationColumn.PollingInterval: - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL - AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'PollingInterval' AND system_type_id = 41) - AND NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__PollingIntervalTicks') - ALTER TABLE [dbo].[MonitoredServices] ADD [__PollingIntervalTicks] bigint NULL; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL - AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'PollingInterval' AND system_type_id = 41) - AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__PollingIntervalTicks') - UPDATE [dbo].[MonitoredServices] - SET [__PollingIntervalTicks] = DATEDIFF_BIG(NANOSECOND, CAST('00:00:00' AS time), [PollingInterval]) / 100; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL - AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'PollingInterval' AND system_type_id = 41) - AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__PollingIntervalTicks') - ALTER TABLE [dbo].[MonitoredServices] ALTER COLUMN [__PollingIntervalTicks] bigint NOT NULL; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL - AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'PollingInterval' AND system_type_id = 41) - AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__PollingIntervalTicks') - BEGIN - ALTER TABLE [dbo].[MonitoredServices] DROP COLUMN [PollingInterval]; - EXEC sp_rename N'dbo.MonitoredServices.__PollingIntervalTicks', N'PollingInterval', N'COLUMN'; - END; - """, cancellationToken); - break; - - case LegacyDurationColumn.RequestTimeout: - await ConvertLegacyMonitoredServiceDurationAsync(dbContext, LegacyDurationColumn.RequestTimeout, cancellationToken); - break; - - case LegacyDurationColumn.RetentionPeriod: - await ConvertLegacyMonitoredServiceDurationAsync(dbContext, LegacyDurationColumn.RetentionPeriod, cancellationToken); - break; - - case LegacyDurationColumn.ResponseDuration: - await ConvertLegacyHealthObservationDurationAsync(dbContext, cancellationToken); - break; - - case LegacyDurationColumn.Duration: - await ConvertLegacyHealthCheckDurationAsync(dbContext, cancellationToken); - break; - - case LegacyDurationColumn.LastResponseDuration: - await ConvertLegacySnapshotDurationAsync(dbContext, cancellationToken); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(migration), migration, "Unknown legacy duration column."); - } - } - - private static async Task ConvertLegacyMonitoredServiceDurationAsync( - HealthMonitoringDbContext dbContext, - LegacyDurationColumn migration, - CancellationToken cancellationToken) - { - if (migration == LegacyDurationColumn.RequestTimeout) - { - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'RequestTimeout' AND system_type_id = 41) AND NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__RequestTimeoutTicks') ALTER TABLE [dbo].[MonitoredServices] ADD [__RequestTimeoutTicks] bigint NULL; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'RequestTimeout' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__RequestTimeoutTicks') UPDATE [dbo].[MonitoredServices] SET [__RequestTimeoutTicks] = DATEDIFF_BIG(NANOSECOND, CAST('00:00:00' AS time), [RequestTimeout]) / 100; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'RequestTimeout' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__RequestTimeoutTicks') ALTER TABLE [dbo].[MonitoredServices] ALTER COLUMN [__RequestTimeoutTicks] bigint NOT NULL; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'RequestTimeout' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__RequestTimeoutTicks') BEGIN ALTER TABLE [dbo].[MonitoredServices] DROP COLUMN [RequestTimeout]; EXEC sp_rename N'dbo.MonitoredServices.__RequestTimeoutTicks', N'RequestTimeout', N'COLUMN'; END; - """, cancellationToken); - } - else - { - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'RetentionPeriod' AND system_type_id = 41) AND NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__RetentionPeriodTicks') ALTER TABLE [dbo].[MonitoredServices] ADD [__RetentionPeriodTicks] bigint NULL; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'RetentionPeriod' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__RetentionPeriodTicks') UPDATE [dbo].[MonitoredServices] SET [__RetentionPeriodTicks] = DATEDIFF_BIG(NANOSECOND, CAST('00:00:00' AS time), [RetentionPeriod]) / 100; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'RetentionPeriod' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__RetentionPeriodTicks') ALTER TABLE [dbo].[MonitoredServices] ALTER COLUMN [__RetentionPeriodTicks] bigint NOT NULL; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[MonitoredServices]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'RetentionPeriod' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MonitoredServices]') AND name = N'__RetentionPeriodTicks') BEGIN ALTER TABLE [dbo].[MonitoredServices] DROP COLUMN [RetentionPeriod]; EXEC sp_rename N'dbo.MonitoredServices.__RetentionPeriodTicks', N'RetentionPeriod', N'COLUMN'; END; - """, cancellationToken); - } - } - - private static async Task ConvertLegacyHealthObservationDurationAsync(HealthMonitoringDbContext dbContext, CancellationToken cancellationToken) - { - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[HealthObservations]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthObservations]') AND name = N'ResponseDuration' AND system_type_id = 41) AND NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthObservations]') AND name = N'__ResponseDurationTicks') ALTER TABLE [dbo].[HealthObservations] ADD [__ResponseDurationTicks] bigint NULL; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[HealthObservations]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthObservations]') AND name = N'ResponseDuration' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthObservations]') AND name = N'__ResponseDurationTicks') UPDATE [dbo].[HealthObservations] SET [__ResponseDurationTicks] = DATEDIFF_BIG(NANOSECOND, CAST('00:00:00' AS time), [ResponseDuration]) / 100; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[HealthObservations]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthObservations]') AND name = N'ResponseDuration' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthObservations]') AND name = N'__ResponseDurationTicks') ALTER TABLE [dbo].[HealthObservations] ALTER COLUMN [__ResponseDurationTicks] bigint NOT NULL; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[HealthObservations]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthObservations]') AND name = N'ResponseDuration' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthObservations]') AND name = N'__ResponseDurationTicks') BEGIN ALTER TABLE [dbo].[HealthObservations] DROP COLUMN [ResponseDuration]; EXEC sp_rename N'dbo.HealthObservations.__ResponseDurationTicks', N'ResponseDuration', N'COLUMN'; END; - """, cancellationToken); - } - - private static async Task ConvertLegacyHealthCheckDurationAsync(HealthMonitoringDbContext dbContext, CancellationToken cancellationToken) - { - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[HealthCheckResults]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthCheckResults]') AND name = N'Duration' AND system_type_id = 41) AND NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthCheckResults]') AND name = N'__DurationTicks') ALTER TABLE [dbo].[HealthCheckResults] ADD [__DurationTicks] bigint NULL; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[HealthCheckResults]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthCheckResults]') AND name = N'Duration' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthCheckResults]') AND name = N'__DurationTicks') UPDATE [dbo].[HealthCheckResults] SET [__DurationTicks] = DATEDIFF_BIG(NANOSECOND, CAST('00:00:00' AS time), [Duration]) / 100; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[HealthCheckResults]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthCheckResults]') AND name = N'Duration' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthCheckResults]') AND name = N'__DurationTicks') ALTER TABLE [dbo].[HealthCheckResults] ALTER COLUMN [__DurationTicks] bigint NOT NULL; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[HealthCheckResults]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthCheckResults]') AND name = N'Duration' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[HealthCheckResults]') AND name = N'__DurationTicks') BEGIN ALTER TABLE [dbo].[HealthCheckResults] DROP COLUMN [Duration]; EXEC sp_rename N'dbo.HealthCheckResults.__DurationTicks', N'Duration', N'COLUMN'; END; - """, cancellationToken); - } - - private static async Task ConvertLegacySnapshotDurationAsync(HealthMonitoringDbContext dbContext, CancellationToken cancellationToken) - { - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[ServiceStatusSnapshots]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[ServiceStatusSnapshots]') AND name = N'LastResponseDuration' AND system_type_id = 41) AND NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[ServiceStatusSnapshots]') AND name = N'__LastResponseDurationTicks') ALTER TABLE [dbo].[ServiceStatusSnapshots] ADD [__LastResponseDurationTicks] bigint NULL; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[ServiceStatusSnapshots]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[ServiceStatusSnapshots]') AND name = N'LastResponseDuration' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[ServiceStatusSnapshots]') AND name = N'__LastResponseDurationTicks') UPDATE [dbo].[ServiceStatusSnapshots] SET [__LastResponseDurationTicks] = DATEDIFF_BIG(NANOSECOND, CAST('00:00:00' AS time), [LastResponseDuration]) / 100; - """, cancellationToken); - await dbContext.Database.ExecuteSqlRawAsync(""" - IF OBJECT_ID(N'[dbo].[ServiceStatusSnapshots]', N'U') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[ServiceStatusSnapshots]') AND name = N'LastResponseDuration' AND system_type_id = 41) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[ServiceStatusSnapshots]') AND name = N'__LastResponseDurationTicks') BEGIN ALTER TABLE [dbo].[ServiceStatusSnapshots] DROP COLUMN [LastResponseDuration]; EXEC sp_rename N'dbo.ServiceStatusSnapshots.__LastResponseDurationTicks', N'LastResponseDuration', N'COLUMN'; END; - """, cancellationToken); - } - - private enum LegacyDurationColumn - { - PollingInterval, - RequestTimeout, - RetentionPeriod, - ResponseDuration, - Duration, - LastResponseDuration - } -} - -#pragma warning restore EF1002 diff --git a/HealthMonitoring/Program.cs b/HealthMonitoring/Program.cs index 6315a02..30983b3 100644 --- a/HealthMonitoring/Program.cs +++ b/HealthMonitoring/Program.cs @@ -34,8 +34,10 @@ var connectionString = app.Configuration.GetConnectionString("HealthMonitoring"); if (!string.IsNullOrWhiteSpace(connectionString)) { - await HealthMonitoringSchemaInitializer.InitializeAsync( - scope.ServiceProvider.GetRequiredService()); + await scope.ServiceProvider + .GetRequiredService() + .Database + .MigrateAsync(); } } diff --git a/README.md b/README.md index dfd8ff4..4657309 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ Run locally with: dotnet run --project HealthMonitoring/HealthMonitoring.csproj ``` -Set `ConnectionStrings:HealthMonitoring` (or `HealthMonitoring__ConnectionStrings__HealthMonitoring`) to a SQL Server connection string before enabling persistence and migrations. +Set `ConnectionStrings:HealthMonitoring` (or `HealthMonitoring__ConnectionStrings__HealthMonitoring`) to a SQL Server connection string before enabling persistence and migrations. The application applies pending EF Core migrations on startup. The checked-in local sample uses SQL authentication with user `sa` and the dummy password `ChangeThisStrongPassword!123`. Replace it with the actual local `sa` password, preferably through an environment variable or user secrets. -The application creates its schema on startup for the first version. A local SQL Server plus the monitor can be started with: +A local SQL Server plus the monitor can be started with: ```powershell docker compose -f HealthMonitoring/docker-compose.sqlserver.yml up --build From e934dcb5aa5d36a893da9b662e7ed77332412a43 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Sat, 19 Sep 2026 11:00:32 +0100 Subject: [PATCH 2/2] chore: exclude EF migrations from Codacy analysis --- .codacy.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .codacy.yml diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 0000000..4007f9a --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,3 @@ +--- +exclude_paths: + - "HealthMonitoring/Migrations/**"