Integrate PostgreSQL with EF Core & migrations

Add PostgreSQL support and EF Core migrations for local development. Introduces ApplicationDbContext, adds Npgsql.EntityFrameworkCore.PostgreSQL and EF Core Design packages, and includes generated migrations (InitialPostgres and RemoveTestItems) plus updated model snapshot. Program.cs now loads an optional API/appsettings.custom.json, configures the DbContext from ConnectionStrings:Postgres and runs Database.Migrate() on startup. Adds a Dev docker-compose.yml to run postgres + pgAdmin, pins dotnet-ef in dotnet-tools.json, and adds ConnectionStrings to appsettings files. Also ignores API/appsettings.custom.json in .gitignore and updates README/codexInfo to document the new DB/dev workflow.
This commit is contained in:
Jonas
2026-04-18 17:40:42 +02:00
parent db8ed2a868
commit fcd2dca8dc
15 changed files with 293 additions and 1 deletions
+5
View File
@@ -7,6 +7,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
+5
View File
@@ -0,0 +1,5 @@
using Microsoft.EntityFrameworkCore;
namespace API.Database;
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options);
+38
View File
@@ -0,0 +1,38 @@
services:
postgres:
image: postgres:16-alpine
container_name: hoard-postgres
restart: unless-stopped
environment:
POSTGRES_DB: hoard
POSTGRES_USER: hoard
POSTGRES_PASSWORD: hoard_dev_password
ports:
- "5432:5432"
volumes:
- hoard_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hoard -d hoard"]
interval: 10s
timeout: 5s
retries: 5
pgadmin:
image: dpage/pgadmin4:9.3
container_name: hoard-pgadmin
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: familiehimmelberg@gmail.com
PGADMIN_DEFAULT_PASSWORD: admin
PGADMIN_CONFIG_SERVER_MODE: "False"
ports:
- "5050:80"
depends_on:
postgres:
condition: service_healthy
volumes:
- hoard_pgadmin_data:/var/lib/pgadmin
volumes:
hoard_postgres_data:
hoard_pgadmin_data:
@@ -0,0 +1,58 @@
// <auto-generated />
using System;
using API.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260418134949_InitialPostgres")]
partial class InitialPostgres
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Models.Test.TestItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime>("UpdatedAtUtc")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("test_items", (string)null);
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,39 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class InitialPostgres : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "test_items",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
CreatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_test_items", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "test_items");
}
}
}
@@ -0,0 +1,29 @@
// <auto-generated />
using API.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260418153650_RemoveTestItems")]
partial class RemoveTestItems
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,39 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class RemoveTestItems : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "test_items");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "test_items",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
CreatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
UpdatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_test_items", x => x.Id);
});
}
}
}
@@ -0,0 +1,26 @@
// <auto-generated />
using API.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
#pragma warning restore 612, 618
}
}
}
+18
View File
@@ -1,10 +1,28 @@
using API.Database;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.custom.json", optional: true, reloadOnChange: true);
var connectionString = builder.Configuration.GetConnectionString("Postgres")
?? throw new InvalidOperationException("Connection string 'Postgres' wurde nicht gefunden.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(connectionString);
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
dbContext.Database.Migrate();
}
var webRootPath = app.Environment.WebRootPath ?? Path.Combine(app.Environment.ContentRootPath, "wwwroot");
var indexFilePath = Path.Combine(webRootPath, "index.html");
+3
View File
@@ -1,4 +1,7 @@
{
"ConnectionStrings": {
"Postgres": "Host=localhost;Port=5432;Database=hoard;Username=hoard;Password=hoard_dev_password"
},
"Logging": {
"LogLevel": {
"Default": "Information",
+3
View File
@@ -1,4 +1,7 @@
{
"ConnectionStrings": {
"Postgres": ""
},
"Logging": {
"LogLevel": {
"Default": "Information",