docs: update roadmap, architecture, ADRs and README for phase 1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3b36dce605
commit
fe7067be93
7 files changed
+156
-35
No files matched your search
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"PowerShell(aspire *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,26 @@ Digitaler Familienplaner (PWA) auf .NET 10 + Aspire 13. Anforderungen: `docs/ARC
|
||||
## Befehle
|
||||
|
||||
```bash
|
||||
dotnet build FamilyPlanner.slnx # muss ohne Warnungen bauen (TreatWarningsAsErrors)
|
||||
dotnet test FamilyPlanner.slnx # alle Tests; Integrationstests brauchen Docker
|
||||
dotnet test tests/FamilyPlanner.UnitTests # schnell, ohne Docker
|
||||
aspire run # bzw. dotnet run --project src/FamilyPlanner.AppHost
|
||||
aspire publish -o artifacts/compose # Docker-Compose aus dem App-Modell erzeugen
|
||||
dotnet build FamilyPlanner.slnx # muss ohne Warnungen bauen (TreatWarningsAsErrors)
|
||||
dotnet test --solution FamilyPlanner.slnx # alle Tests (Microsoft.Testing.Platform); Integrationstests brauchen Docker
|
||||
dotnet test --project tests/FamilyPlanner.UnitTests # schnell, ohne Docker
|
||||
dotnet test --project tests/FamilyPlanner.Web.Tests # bUnit
|
||||
dotnet test --project tests/FamilyPlanner.IntegrationTests # Aspire + SQL-Server-Container (~1–2 min)
|
||||
aspire run # bzw. dotnet run --project src/FamilyPlanner.AppHost
|
||||
aspire publish -o artifacts/compose # Docker-Compose aus dem App-Modell (Aspire-CLI >= 13.6)
|
||||
# ohne passende CLI:
|
||||
dotnet run --project src/FamilyPlanner.AppHost --no-launch-profile -- --operation publish --step publish --output-path ../../artifacts/compose
|
||||
|
||||
# Neue EF-Migration (aus dem Repo-Root)
|
||||
# Neue EF-Migration (aus dem Repo-Root; Tool über dotnet-tools.json: `dotnet tool restore`)
|
||||
dotnet ef migrations add <Name> -p src/FamilyPlanner.Infrastructure -s src/FamilyPlanner.MigrationService -o Persistence/Migrations
|
||||
|
||||
# Container-Images bauen
|
||||
dotnet publish src/FamilyPlanner.Api -c Release /t:PublishContainer
|
||||
dotnet publish src/FamilyPlanner.Web -c Release /t:PublishContainer
|
||||
dotnet publish src/FamilyPlanner.MigrationService -c Release /t:PublishContainer
|
||||
# Produktiv-Stack lokal bauen/starten
|
||||
cd deploy && cp .env.example .env && docker compose up -d --build
|
||||
```
|
||||
|
||||
Wichtig: Wenn der AppHost läuft, sind die DLLs von Api/Web gesperrt → vor `dotnet build`/Tests beenden.
|
||||
Die Integrationstests starten einen eigenen, flüchtigen SQL-Container; der Dev-AppHost nutzt das Volume `familyplanner-sql-data`.
|
||||
|
||||
Lokaler Seed-Admin (AppHost-Parameter, siehe `src/FamilyPlanner.AppHost/appsettings.Development.json`): `admin` / `Admin123!ChangeMe` – Passwortwechsel beim ersten Login.
|
||||
|
||||
## Struktur
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Familienplaner
|
||||
|
||||
Digitaler Familienplaner als Progressive Web App: Einkaufsliste, Essenplan, Aufgaben & Ämtchen, Familienkalender
|
||||
(inkl. Abo für iPhone/Outlook) und Chat – für Smartphone, Tablet und Desktop.
|
||||
|
||||
> **Status:** Phase 1 (Fundament) ist umgesetzt: Login, Rollen, Navigation, PWA-Grundgerüst, Aspire- und Docker-Betrieb.
|
||||
> Die Fachfunktionen folgen phasenweise, siehe [`docs/ROADMAP.md`](docs/ROADMAP.md).
|
||||
|
||||
* Architektur: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
||||
* Entscheidungen: [`docs/adr/`](docs/adr/)
|
||||
|
||||
## Voraussetzungen (Entwicklung)
|
||||
|
||||
* .NET SDK 10 (getestet mit 10.0.300)
|
||||
* Docker Desktop bzw. Docker Engine (für SQL Server)
|
||||
* optional: Aspire CLI (`aspire run`)
|
||||
|
||||
## Entwicklung
|
||||
|
||||
```bash
|
||||
dotnet tool restore
|
||||
aspire run # oder: dotnet run --project src/FamilyPlanner.AppHost
|
||||
```
|
||||
|
||||
Das Aspire-Dashboard öffnet sich automatisch (Login-Link steht in der Konsole). Die App läuft unter
|
||||
<https://localhost:7264>. Beim ersten Start wird der SQL-Server-Container mit persistentem Volume angelegt,
|
||||
der MigrationService migriert die Datenbank und legt den Initial-Admin an:
|
||||
|
||||
| Benutzer | Passwort | Hinweis |
|
||||
|---|---|---|
|
||||
| `admin` | `Admin123!ChangeMe` | nur lokal (`src/FamilyPlanner.AppHost/appsettings.Development.json`), muss beim ersten Login geändert werden |
|
||||
|
||||
Das Passwort lässt sich per User Secrets überschreiben:
|
||||
`dotnet user-secrets set "Parameters:seed-admin-password" "..." --project src/FamilyPlanner.AppHost`.
|
||||
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
dotnet build FamilyPlanner.slnx
|
||||
dotnet test --solution FamilyPlanner.slnx # Integrationstests benötigen Docker
|
||||
```
|
||||
|
||||
## Produktivbetrieb (Docker Compose)
|
||||
|
||||
```bash
|
||||
cd deploy
|
||||
cp .env.example .env # Passwörter und Domain anpassen
|
||||
docker compose up -d --build # ohne Reverse Proxy, Web auf 127.0.0.1:8080
|
||||
# oder mit automatischem HTTPS (Caddy + Let's Encrypt, Ports 80/443):
|
||||
docker compose --profile proxy up -d --build
|
||||
```
|
||||
|
||||
Die App benötigt **HTTPS** (sichere Cookies). Ohne Caddy funktioniert der Login nur über `http://localhost` oder hinter
|
||||
einem eigenen Reverse Proxy mit TLS.
|
||||
|
||||
### Konfiguration (`deploy/.env`)
|
||||
|
||||
| Variable | Bedeutung |
|
||||
|---|---|
|
||||
| `SQL_SA_PASSWORD` | Passwort des SQL-Server-Administrators (komplex, min. 8 Zeichen) |
|
||||
| `MSSQL_PID` | SQL-Server-Edition (`Express` = kostenlos) |
|
||||
| `ADMIN_USERNAME`, `ADMIN_PASSWORD`, `ADMIN_EMAIL` | Initial-Admin, nur beim allerersten Start; Passwortwechsel wird erzwungen |
|
||||
| `FAMILY_NAME` | Familienname (Vorbelegung der Familieneinstellungen beim ersten Start) |
|
||||
| `WEB_BIND`, `WEB_PORT` | Host-Bindung des Web-Containers ohne Caddy |
|
||||
| `DOMAIN` | Domain für Caddy/Let's Encrypt (Profil `proxy`) |
|
||||
| `REGISTRY`, `TAG` | optionales Image-Präfix und Tag |
|
||||
|
||||
Weitere Einstellungen (alle Dienste) folgen der ASP.NET-Core-Konvention `Abschnitt__Schlüssel`, z. B.
|
||||
`RateLimiting__LoginPermitLimit=10` (Api).
|
||||
|
||||
### Update
|
||||
|
||||
```bash
|
||||
cd deploy
|
||||
git pull
|
||||
docker compose up -d --build # Migrationen laufen automatisch über den migrations-Container
|
||||
```
|
||||
|
||||
### Backup & Restore
|
||||
|
||||
```bash
|
||||
cd deploy
|
||||
./scripts/backup.sh # schreibt backups/familyplanner-<datum>.bak
|
||||
./scripts/restore.sh backups/familyplanner-20261009-120000.bak
|
||||
```
|
||||
|
||||
Backups liegen zusätzlich im Docker-Volume `sql-backup` (14 Tage). Für eine regelmäßige Sicherung z. B. per Cron:
|
||||
`0 3 * * * cd /opt/familyplanner/deploy && ./scripts/backup.sh`.
|
||||
|
||||
## Kalender-Abos (iPhone/Outlook)
|
||||
|
||||
Folgt in Phase 6 (ICS-Feed mit geheimem Abo-Link, siehe [ADR-0005](docs/adr/0005-ics-feed-token.md)).
|
||||
+10
-4
@@ -50,7 +50,9 @@ Siehe ADRs in [`docs/adr/`](adr/) für die Begründungen.
|
||||
| `src/FamilyPlanner.Web` | Blazor-Host/BFF: statische Auslieferung der WASM-App + YARP-Proxy |
|
||||
| `src/FamilyPlanner.Web.Client` | Blazor WebAssembly (MudBlazor), PWA |
|
||||
| `tests/FamilyPlanner.UnitTests` | Domänenlogik, Rollen/Policies, Parser, ICS |
|
||||
| `tests/FamilyPlanner.IntegrationTests` | End-to-End über `Aspire.Hosting.Testing` (echter SQL-Server-Container) |
|
||||
| `tests/FamilyPlanner.IntegrationTests` | End-to-End über `Aspire.Hosting.Testing` (echter SQL-Server-Container, flüchtige DB) |
|
||||
| `deploy/` | `docker-compose.yml`, `.env.example`, `Caddyfile`, Backup-/Restore-Skripte |
|
||||
| `tools/` | Hilfsskripte (z. B. `make_icons.py` zum Erzeugen der PWA-Icons) |
|
||||
| `tests/FamilyPlanner.Web.Tests` | bUnit-Komponententests |
|
||||
|
||||
### Vertical Slices in der Api
|
||||
@@ -141,7 +143,9 @@ Ein Benutzer mit `MustChangePassword = true` erhält nur Zugriff auf `/api/auth/
|
||||
## 7. Frontend
|
||||
|
||||
* Blazor Web App, **Interactive WebAssembly global, Prerendering aus** (ADR-0002). Der Web-Host rendert nur die HTML-Hülle.
|
||||
* MudBlazor als Komponentenbibliothek (lokal ausgeliefert, kein CDN), deutsches UI, Texte vorbereitet für Lokalisierung (`IStringLocalizer`, `Resources/`).
|
||||
* MudBlazor als Komponentenbibliothek (lokal ausgeliefert, kein CDN), deutsches UI. Lokalisierung vorbereitet: Kultur `de-DE`,
|
||||
`AddLocalization()` registriert, alle Globalisierungsdaten geladen; Texte stehen derzeit direkt in den Komponenten und werden bei Bedarf in `Resources/*.resx` überführt.
|
||||
* Ordner `Pages/` erfordert per `_Imports.razor` die Policy `PasswordChanged`; `Public/` enthält Login, Passwortwechsel und 404.
|
||||
* Layout: MudLayout mit AppBar, Drawer (Desktop) bzw. Bottom-Navigation (Smartphone), Dark Mode (System-Default, umschaltbar, in `localStorage`).
|
||||
* Eigener `AuthenticationStateProvider` (`ApiAuthenticationStateProvider`) holt `/api/auth/me` und liefert Claims inkl. Rollen.
|
||||
* PWA: `manifest.webmanifest`, Icons 72–512 px + maskable, eigener Service Worker (App-Shell-Cache, Network-first für Navigation, keine Caches für `/api`/`/hubs`), Offline-Banner. Hintergrund-Sync für die Einkaufsliste über IndexedDB-Outbox ab Phase 3.
|
||||
@@ -155,10 +159,12 @@ SignalR-Hubs in der Api, vom Web-Host per YARP (WebSockets) durchgereicht. Grupp
|
||||
|
||||
* **Lokal:** `aspire run` bzw. `dotnet run --project src/FamilyPlanner.AppHost`.
|
||||
* **Produktion:** `deploy/docker-compose.yml` (SQL Server + Volume, migrations, api, web, optional Caddy für HTTPS).
|
||||
Images werden über die SDK-Container-Unterstützung (`dotnet publish /t:PublishContainer`) bzw. Aspire-Publishing gebaut; alle Container laufen als Non-Root (`app`-User der Microsoft-Images).
|
||||
Images werden mit dem gemeinsamen `Dockerfile` (Build-Argument `PROJECT`) gebaut; alle Container laufen als Non-Root (`app`-User der Microsoft-Images, SQL Server als `mssql`).
|
||||
* Backups: `deploy/scripts/backup.sh` (SQL-Backup ins Volume `sql-backup` + Kopie nach `deploy/backups/`), `deploy/scripts/restore.sh`.
|
||||
* `aspire publish` mit `AddDockerComposeEnvironment("compose")` erzeugt alternativ eine Compose-Datei aus dem App-Modell.
|
||||
* Konfiguration ausschließlich per Umgebungsvariablen/`.env` (siehe `deploy/.env.example`), lokal User Secrets / Aspire-Parameter.
|
||||
* Health Checks: `/alive` (Liveness), `/health` (Readiness inkl. DB) – in Produktion nur vom Docker-Netz bzw. per Health-Check-Befehl erreichbar, der Web-Host proxyt sie nicht.
|
||||
* Health Checks: `/alive` (Liveness), `/health` (Readiness inkl. DB bei der Api) – auch in Produktion gemappt (nur Status-Text, keine Details).
|
||||
Der Web-Host beantwortet `/health` selbst und leitet es nicht an die Api weiter.
|
||||
* Data Protection Keys werden in der Datenbank gespeichert (Api), damit Cookies Container-Neustarts überleben.
|
||||
|
||||
## 10. Querschnitt
|
||||
|
||||
+18
-15
@@ -3,20 +3,21 @@
|
||||
Legende: `[x]` erledigt, `[ ]` offen. Nach jeder Phase: Build ohne Warnungen, Tests grün, Start über Aspire, Commit.
|
||||
|
||||
## Phase 1 – Fundament
|
||||
- [ ] Solution (`FamilyPlanner.slnx`), alle Projekte laut Architektur inkl. MigrationService
|
||||
- [ ] `Directory.Build.props` (Nullable, TreatWarningsAsErrors), `Directory.Packages.props`, `.editorconfig`, `global.json`, `.gitignore`
|
||||
- [ ] Aspire AppHost: SQL Server mit persistentem Volume, Datenbank-Ressource, MigrationService, Api, Web (`WithReference`, `WaitFor`, `WaitForCompletion`)
|
||||
- [ ] ServiceDefaults: OpenTelemetry, `/health`, `/alive`, Resilience, Service Discovery
|
||||
- [ ] EF Core `AppDbContext` mit Identity, `AuditableEntity`, Soft Delete, Audit-Interceptor, Initial-Migration
|
||||
- [ ] Data Protection Keys in der Datenbank
|
||||
- [ ] Seeding: Rollen Admin/Familie/Kind/Gast, Initial-Admin aus Konfiguration mit erzwungener Passwortänderung, Familieneinstellungen
|
||||
- [ ] Auth-API: Login (Benutzername/E-Mail, Angemeldet bleiben), Logout, Me, Passwort ändern; Lockout; Rate Limiting
|
||||
- [ ] Web-Host als BFF mit YARP (`/api`, `/hubs`, `/ics`)
|
||||
- [ ] Blazor-Shell mit MudBlazor: Login, Passwortwechsel, Navigation, Platzhalterseiten, Dark Mode, deutsche Texte
|
||||
- [ ] PWA-Grundgerüst: Manifest, Icons, Service Worker, Offline-Hinweis
|
||||
- [ ] Docker: Container-Images (non-root), `deploy/docker-compose.yml`, `deploy/.env.example`, Aspire-Docker-Compose-Publishing
|
||||
- [ ] Tests: Unit (Rollen/Domain), Integration (Login-Flow über Aspire.Hosting.Testing), bUnit (Login-Seite)
|
||||
- [ ] CI-Pipeline (GitHub Actions)
|
||||
- [x] Solution (`FamilyPlanner.slnx`), alle Projekte laut Architektur inkl. MigrationService
|
||||
- [x] `Directory.Build.props` (Nullable, TreatWarningsAsErrors), `Directory.Packages.props`, `.editorconfig`, `global.json`, `.gitignore`
|
||||
- [x] Aspire AppHost: SQL Server mit persistentem Volume, Datenbank-Ressource, MigrationService, Api, Web (`WithReference`, `WaitFor`, `WaitForCompletion`)
|
||||
- [x] ServiceDefaults: OpenTelemetry, `/health`, `/alive`, Resilience, Service Discovery
|
||||
- [x] EF Core `AppDbContext` mit Identity, `AuditableEntity`, Soft Delete, Audit-Interceptor, Initial-Migration
|
||||
- [x] Data Protection Keys in der Datenbank
|
||||
- [x] Seeding: Rollen Admin/Familie/Kind/Gast, Initial-Admin aus Konfiguration mit erzwungener Passwortänderung, Familieneinstellungen
|
||||
- [x] Auth-API: Login (Benutzername/E-Mail, Angemeldet bleiben), Logout, Me, Passwort ändern; Lockout; Rate Limiting; CSRF-Header
|
||||
- [x] Web-Host als BFF mit YARP (`/api`, `/hubs`, `/ics`)
|
||||
- [x] Blazor-Shell mit MudBlazor: Login, Passwortwechsel, Navigation (Drawer + Bottom-Navigation), Platzhalterseiten, Dark Mode, deutsche Texte
|
||||
- [x] PWA-Grundgerüst: Manifest, Icons, Service Worker, Offline-Hinweis
|
||||
- [x] Docker: Container-Images (non-root), `deploy/docker-compose.yml`, `deploy/.env.example`, Aspire-Docker-Compose-Publishing
|
||||
- [x] Backup-/Restore-Skripte (erste Version), README (erste Version)
|
||||
- [x] Tests: Unit (Rollen/Policies/Domain/Interceptor), Integration (Login-Flow über Aspire.Hosting.Testing), bUnit (Login-Seite)
|
||||
- [x] CI-Pipeline (GitHub Actions)
|
||||
|
||||
## Phase 2 – Rollen & Admin
|
||||
- [ ] Policies vollständig (siehe ARCHITECTURE §4) inkl. Unit-Tests je Policy
|
||||
@@ -79,7 +80,9 @@ Legende: `[x]` erledigt, `[ ]` offen. Nach jeder Phase: Build ohne Warnungen, Te
|
||||
- [ ] Rechte Kind/Gast
|
||||
|
||||
## Phase 9 – Härtung
|
||||
- [ ] Security-Header (CSP, HSTS, X-Content-Type-Options, Referrer-Policy, Permissions-Policy)
|
||||
- [ ] Content-Security-Policy (Basis-Header X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, HSTS sind seit Phase 1 aktiv)
|
||||
- [ ] Eigener SQL-Login mit minimalen Rechten statt `sa` für Api/MigrationService
|
||||
- [ ] Service-Worker-Cache-Version automatisch beim Build setzen
|
||||
- [ ] Rate Limiting vervollständigen
|
||||
- [ ] Tests vervollständigen (Policies, ICS, Wiederholungen, Integration)
|
||||
- [ ] Barrierefreiheit (WCAG 2.1 AA), Performance (AOT/Trimming prüfen, Lazy Loading)
|
||||
|
||||
@@ -14,6 +14,8 @@ wenn Browser-App und Api dieselbe Origin haben.
|
||||
* Die **Api besitzt Identity** und stellt das Cookie `__Host-fp.auth` aus (HttpOnly, Secure, SameSite=Strict). Durch den Proxy ist es aus Browsersicht ein First-Party-Cookie.
|
||||
* Rendermodus: **InteractiveWebAssembly global mit `prerender: false`**. Der Web-Host kennt dadurch keinen Auth-Zustand und braucht weder Identity noch geteilte Data-Protection-Keys.
|
||||
* Der WASM-Client nutzt einen eigenen `AuthenticationStateProvider`, der `/api/auth/me` abfragt.
|
||||
* Die Seiten tragen `[Authorize]`-Metadaten für den Client-Router. Damit der Host diese nicht selbst auswertet (er hat kein Auth-Schema),
|
||||
registriert er nur die Policy-Namen und liefert die Razor-Endpunkte mit `.AllowAnonymous()` aus. Die Hülle enthält keine Daten.
|
||||
* CSRF-Schutz: SameSite=Strict plus Pflicht-Header `X-Requested-With` für alle schreibenden API-Requests (ADR-0006).
|
||||
|
||||
## Alternativen
|
||||
|
||||
@@ -4,15 +4,21 @@
|
||||
* Datum: 2026-10-09
|
||||
|
||||
## Entscheidung
|
||||
* .NET 10 (SDK 10.0.300, `global.json` mit `rollForward: latestFeature`), Aspire 13.6.1 (`Aspire.AppHost.Sdk`).
|
||||
* .NET 10 (`global.json`: mindestens 10.0.100, `rollForward: latestFeature`; entwickelt mit SDK 10.0.300), Aspire 13.6.1 (`Aspire.AppHost.Sdk`).
|
||||
* Zentrales Paketmanagement über `Directory.Packages.props`; nur stabile Versionen (Stand 2026-10: EF Core/ASP.NET 10.0.12, MudBlazor 9.11.0,
|
||||
YARP 2.3.0, OpenTelemetry 1.19, FluentValidation 12.1.1, Ical.Net 5.2.3, xunit.v3 4.0.1, bUnit 2.11.3).
|
||||
YARP 2.3.0, Service Discovery/Resilience 10.10.0, OpenTelemetry 1.19, FluentValidation 12.1.1, Ical.Net 5.2.3, xunit.v3 4.0.1, bUnit 2.11.3).
|
||||
`Aspire.Hosting.AppHost` wird implizit vom AppHost-SDK referenziert und steht deshalb nicht in `Directory.Packages.props`.
|
||||
* Solution im XML-Format **`FamilyPlanner.slnx`** (Default des .NET-10-SDK, von Visual Studio, Rider und `dotnet` unterstützt) statt `.sln`.
|
||||
* `Directory.Build.props`: Nullable, ImplicitUsings, `TreatWarningsAsErrors`.
|
||||
* Tests: xUnit v3 (VSTest-Adapter, damit `dotnet test` ohne Zusatzkonfiguration läuft), bUnit für Komponenten,
|
||||
`Aspire.Hosting.Testing` für Integrationstests mit echtem SQL-Server-Container.
|
||||
* Container-Images über die SDK-Container-Unterstützung (`dotnet publish /t:PublishContainer`), Basis `mcr.microsoft.com/dotnet/aspnet:10.0`,
|
||||
Benutzer `app` (non-root, Standard der .NET-8+-Images). Keine handgeschriebenen Dockerfiles nötig.
|
||||
* Tests: xUnit v3 auf **Microsoft.Testing.Platform** (`global.json` → `"test": { "runner": "Microsoft.Testing.Platform" }`).
|
||||
xunit.v3 4.x unterstützt das alte VSTest-Ziel mit dem .NET-10-SDK nicht mehr; daher entfallen `Microsoft.NET.Test.Sdk` und `coverlet.collector`.
|
||||
Aufruf: `dotnet test --solution FamilyPlanner.slnx` bzw. `dotnet test --project <Testprojekt>`.
|
||||
bUnit für Komponenten, `Aspire.Hosting.Testing` für Integrationstests mit echtem SQL-Server-Container (flüchtige DB über `FamilyPlanner:EphemeralDatabase=true`).
|
||||
* Der AppHost nutzt DCP und Dashboard aus den NuGet-Paketen (`AspireUseCliBundle=false`, Warnung ASPIRE010 unterdrückt), damit `dotnet run`
|
||||
und die Integrationstests auch ohne installierte Aspire-CLI laufen (CI).
|
||||
* Container-Images: ein gemeinsames, mehrstufiges `Dockerfile` (Build-Argument `PROJECT`), Basis `mcr.microsoft.com/dotnet/aspnet:10.0`,
|
||||
Benutzer `app` (non-root). Damit kann der Linux-Host die Images ohne installiertes .NET-SDK direkt per `docker compose build` bauen.
|
||||
`curl` ist für die Docker-Health-Checks enthalten.
|
||||
|
||||
## Konsequenzen
|
||||
Werkzeuge, die nur `.sln` lesen, können über `dotnet sln` eine `.sln` erzeugen; die CI und lokale Entwicklung nutzen `.slnx` direkt.
|
||||
Reference in new issue
Block a user