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
+2
View File
@@ -85,6 +85,8 @@ __screenshots__/
.env.local .env.local
.env.*.local .env.*.local
*.local *.local
API/appsettings.custom.json
API/appsettings.custom.*.json
# Temporary files # Temporary files
~$* ~$*
+5
View File
@@ -7,6 +7,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup> </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); 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.AddControllers();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
var app = builder.Build(); 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 webRootPath = app.Environment.WebRootPath ?? Path.Combine(app.Environment.ContentRootPath, "wwwroot");
var indexFilePath = Path.Combine(webRootPath, "index.html"); 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": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
+3
View File
@@ -1,4 +1,7 @@
{ {
"ConnectionStrings": {
"Postgres": ""
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
+1 -1
View File
@@ -1,7 +1,7 @@
# Hoard # Hoard
<p align="center"> <p align="center">
<img src="./logo.png" width="120" /> <img src="./GUI/src/assets/images/icon.png" width="120" />
</p> </p>
Hoard ist eine einfache, selbst gehostete Web-App zur Verwaltung von Dateien und Ordnern mit integrierter Markdown-Bearbeitung direkt im Browser. Hoard ist eine einfache, selbst gehostete Web-App zur Verwaltung von Dateien und Ordnern mit integrierter Markdown-Bearbeitung direkt im Browser.
+14
View File
@@ -90,6 +90,11 @@ Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für
- Backend-API ist auf ein Minimal-Setup reduziert und stellt aktuell den Test-Endpunkt `GET /api/health` bereit. - Backend-API ist auf ein Minimal-Setup reduziert und stellt aktuell den Test-Endpunkt `GET /api/health` bereit.
- Swagger/OpenAPI ist im Backend nur im Development-Modus aktiv (`/swagger`). - Swagger/OpenAPI ist im Backend nur im Development-Modus aktiv (`/swagger`).
- Frontend-Build (`npm run build` im `GUI`-Projekt) schreibt direkt nach `API/wwwroot`; das Backend liefert die SPA und statische Assets aus. - Frontend-Build (`npm run build` im `GUI`-Projekt) schreibt direkt nach `API/wwwroot`; das Backend liefert die SPA und statische Assets aus.
- Backend nutzt jetzt PostgreSQL über `ConnectionStrings:Postgres` mit EF Core (`ApplicationDbContext`) und führt Migrationen beim Start automatisch aus.
- Temporäre Test-Entity und Test-CRUD-Endpunkt (`api/test-items`) wurden wieder entfernt; aktuell bleibt der minimale Health-Endpunkt `GET /api/health`.
- Für lokale Entwicklung liegt unter `API/Dev/docker-compose.yml` ein Stack mit PostgreSQL (`localhost:5432`) und pgAdmin (`localhost:5050`).
- API lädt optional `API/appsettings.custom.json`; wenn vorhanden, überschreibt sie Werte aus `appsettings.json`.
- `API/appsettings.custom.json` ist in `.gitignore` hinterlegt, damit lokale Konfigurationswerte nicht versehentlich committed werden.
## Änderungen durch Codex ## Änderungen durch Codex
- Grundlegender UI-Neuaufbau der App-Shell (`GUI/src/Layout.vue`) inklusive Navigation, Footer und Seitenkontext. - Grundlegender UI-Neuaufbau der App-Shell (`GUI/src/Layout.vue`) inklusive Navigation, Footer und Seitenkontext.
@@ -111,3 +116,12 @@ Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für
- `API/Program.cs` erweitert, damit statische Dateien aus `wwwroot` inkl. SPA-Fallback (`index.html`) ausgeliefert werden. - `API/Program.cs` erweitert, damit statische Dateien aus `wwwroot` inkl. SPA-Fallback (`index.html`) ausgeliefert werden.
- SPA-Fallback im Backend aufgeteilt: Frontend-Routen liefern `index.html`, unbekannte `/api/*`-Routen bleiben korrekt `404` statt auf die SPA zu fallen. - SPA-Fallback im Backend aufgeteilt: Frontend-Routen liefern `index.html`, unbekannte `/api/*`-Routen bleiben korrekt `404` statt auf die SPA zu fallen.
- Swagger im Backend ergänzt: `Swashbuckle.AspNetCore` eingebunden, Services registriert und UI nur in `Development` aktiviert. - Swagger im Backend ergänzt: `Swashbuckle.AspNetCore` eingebunden, Services registriert und UI nur in `Development` aktiviert.
- PostgreSQL-Integration im Backend umgesetzt: `Npgsql.EntityFrameworkCore.PostgreSQL` + `Microsoft.EntityFrameworkCore.Design` in `API/API.csproj` ergänzt und Connection String `ConnectionStrings:Postgres` in den Settings hinterlegt.
- `API/Database/ApplicationDbContext.cs` mit Test-Entity `API/Models/Test/TestItem.cs` angelegt; erste Migrationen in `API/Migrations` erstellt.
- `API/Program.cs` um `AddDbContext` (Postgres) und `Database.Migrate()` beim Start erweitert, damit Migrationen automatisch angewendet werden.
- `API/Controllers/TestItemsController.cs` als einfacher CRUD-Testcontroller (`GET/POST/PUT/DELETE`) unter `api/test-items` ergänzt.
- Dev-Stack für lokale Datenbankarbeit ergänzt: `API/Dev/docker-compose.yml` startet PostgreSQL + pgAdmin.
- `API/Program.cs` so erweitert, dass optional `appsettings.custom.json` geladen wird und bei vorhandener Datei bevorzugte lokale Overrides möglich sind.
- Neue lokale Konfigurationsdatei `API/appsettings.custom.json` aus der bisherigen `appsettings.json` angelegt und in `.gitignore` ergänzt.
- Test-Datentyp und Test-API wieder entfernt: `API/Models/Test/TestItem.cs` und `API/Controllers/TestItemsController.cs` gelöscht, `ApplicationDbContext` bereinigt.
- Neue Migration `RemoveTestItems` erstellt (`API/Migrations/20260418153650_RemoveTestItems.cs`), die Tabelle `test_items` entfernt und den Snapshot aktualisiert.
+13
View File
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.6",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}