↓ Zum Hauptinhalt springen

🧠 AptiGo – technische Dokumentation

·1813 Wörter·9 min

Initialisiert: 11.08.2026 Quelle: Repository-Exploration (Program.cs, Projekt-/csproj-Dateien, CodingStandards.md, .github/copilot-instructions.md, Git-Historie) Zweck: Persistentes GedĂ€chtnis fĂŒr KI-Assistenz (Cline). Bei Bedarf aktualisieren, wenn sich Architektur/Entscheidungen Ă€ndern.


1. ProjektĂŒberblick
#

AptiGo ist eine deutschsprachige Lernplattform fĂŒr Schulen (Lehrer & SchĂŒler). Lehrer erstellen/verwalten Lerninhalte (Content), bĂŒndeln sie zu Kapiteln (Chapters) und Kursen (Courses) und weisen sie SchĂŒlern zu. SchĂŒler absolvieren die Inhalte, sammeln Punkte, TrophĂ€en und Exp. Ein Marktplatz/Shop mit Lizenzen (Monetarisierung) sowie ein Paywall-/Subscription-System (kostenloses SchĂŒler-Kontingent vs. Premium-Tiers) gehören zum Produkt.

Kernrollen: Student und Teacher (Rolle auf ApplicationUser.Role), optional Schulen (School).


2. Technologie-Stack
#

Bereich Technologie
Plattform .NET 10 (net10.0)
UI Blazor Web App (Server + WebAssembly interaktiv, Hybrid)
Rendermodi InteractiveServer + InteractiveWebAssembly; Seiten benötigen explizit @rendermode InteractiveWebAssembly
DB PostgreSQL via EF Core (Npgsql.EntityFrameworkCore.PostgreSQL 10.0.2); lokal zusÀtzlich SQLite-Datei aptigo.db (gitignored, nur legacy/Fallback)
Identity ASP.NET Core Identity mit Guid-Keys (IdentityUser<Guid>, IdentityRole<Guid>)
Auth Individuelle Blazor-UI (kein eingebautes Identity-Razor-UI)
KI Google GenAI (gemini-flash-lite / gemini-2.5-flash-lite) + DeepSeek (deepseek-chat) ĂŒber Microsoft.Extensions.AI
Zahlung Stripe (Stripe.net 51.1.0, Test-API-Key in Config)
Mail SMTP direkt ĂŒber Gmail (MailKit, SmtpEmailSender) – kein SendGrid mehr
Markdown/Formeln Markdig (Markdig.Signed), KaTeX (statisch in wwwroot/lib/katex)
Editor Quill.js 1.3.6 (CDN) mit Eigen-Scripts (aptigo-quill-*.js)
Animation Lottie (Blazor.Lottie.Player 1.0.6), canvas-confetti
Solution AptiGo.slnx (neues XML-SLN-Format) mit 3 Projekten

Projekte
#

  • AptiGo/AptiGo/AptiGo.csproj – Server/Web-Bootstrapping (Program.cs, REST-Controller, Background-Jobs, Data, Services)
  • AptiGo/AptiGo.Client/AptiGo.Client.csproj – Blazor WebAssembly Client (Pages, Components, Client-Services, Layouts)
  • AptiGo.Shared/AptiGo.Shared.csproj – Geteilte Logik (Models/EntitĂ€ten, Interfaces, DTOs, ContentTypes)

⚠ Dateipfade: ContentData.cs liegt in AptiGo.Shared/Models/ContentData.cs, Content.cs in AptiGo.Shared/Models/ContentTypes/Content.cs.


3. Projektstruktur
#

AptiGo/                          # Server-Projekt (Root)
├── Program.cs                   # DI-Registrierungen, Startup, Seeding
├── appsettings.json             # Verbindungen, Keys, Subscription-Konfig (gitignored!)
├── Data/                        # EF-Core Kontext + Profile
│   ├── ApplicationDbContext.cs
│   ├── ApplicationUser.cs
│   ├── TeacherProfile.cs        # Paywall-Felder, RowVersion (xmin)
│   ├── StudentProfile.cs
│   └── IUserContext.cs
├── Services/                    # Server-Dienste + Controllers (*Controller.cs)
│   ├── SubscriptionService.cs   # Paywall-Kernlogik
│   ├── ContentAcessPolicy.cs    # IAccessPolicy (Zugriffsregeln)
│   ├── DBAssetService.cs, DBChapterService.cs, DBContentService.cs, ...
│   ├── AiContentService.cs, DeepSeekAiContentService.cs, GoogleAiContentService.cs
│   ├── ContentMetadataJob.cs, SubscriptionExpiryJob.cs   # Background-HostedServices
│   └── ConfigManager.cs, UserContext.cs, SmtpEmailSender.cs, ...
├── Components/
│   ├── App.razor                # Head, Scripts, Theme via IConfigService
│   ├── Routes.razor             # Router + AuthorizeRouteView (DefaultLayout MainLayout)
│   ├── DesignInjector.razor, PathChaptersViewer.razor
├── Migrations/                  # PostgresInit (06/2026), PaywallSubscription (08/2026)
├── wwwroot/                     # AptiGo.css (ZENTRALES Stylesheet!), assets/, lib/katex
└── Components/Account/          # Identity-Endpoints (Passkey/Login)

AptiGo.Client/                   # WASM-Client
├── Pages/                       # StudentChapterView(New).razor, CourseView, TeacherDashboard, Shop, ...
├── Components/                  # ContentInjector, AptigoQuillEditor, KTexDisplay, ...
├── Services/                    # HTTP-Client-Wrapper fĂŒr die Server-Controller
├── Layout/
└── wwwroot/                     # appsettings*.json (Client-seitige Config)

AptiGo.Shared/                   # Geteilte Schnittstellen + Modelle
├── Interfaces/                  # IContentService, IChapterService, ISubscriptionService, ...
├── Models/                      # EntitĂ€ten + DTOs
└── Models/ContentTypes/         # Abgeleitete ContentData-Klassen

4. Datenmodell (Kern-EntitÀten)
#

ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid> – Namespace AptiGo.Data, Projekt Server.

Benutzer & Profile
#

  • ApplicationUser (IdentityUser<Guid>): DisplayName, AboutMe, Role (“Student”|“Teacher”), ProfilePictureUrl, RealName, IcognitoMode (Default true), IsActive. NotMapped: TotalExp, AptigyBalance, CreditBalance, Balance (Credits in Cent).
  • StudentProfile (PK=UserId, 1:1 zu User): TotalExp, DateOfBirth, SchoolId, PendingSchool, AssignedTeacherId.
  • TeacherProfile (PK=UserId, 1:1 zu User): AptigyBalance, CreditBalance (Anzeige in Cent), SchoolId, PendingSchool. Paywall-Felder: MaxStudents (Default SubscriptionDefaults.FreeTierMaxStudents), CurrentStudentsCount, SubscriptionEndDate, StudentLimitExceeded, RowVersion (uint, IsRowVersion() → xmin bei Postgres).

Lerninhalte
#

  • Content (PK long ID): Title, Type (int → ContentType), Niveau (int → DifficultyLevel), ContentText (JSON-String!), Points, ContentState, Version/Metadaten (CreatedByUserId, LastEditedByUserId, IsVerified, UsageCount, Rating, SchoolId, FlaggedMessage), Nav: Tags, ContentChapters, LicensedContents.
    • SetContentData(ContentData) / GetContentData() serialisieren den Typ-spezifischen Inhalt in ContentText (JSON) und zurĂŒck.
  • ContentData (abstrakt, AptiGo.Shared/Models/ContentData.cs): abstract ContentType Type, abstract string Serialize(), virtual NormalizeAfterAi(), statisch FromJson(type, json) (switch ĂŒber alle ContentTypes) und IsValidJson. KI-Output wird ĂŒber IsValidJson als strikt validiertes JSON geprĂŒft.
  • Chapter (PK long ID): Name, Description, State (Draft|Published|SchoolPublished|Archived), CategoryID, Rating, IsVerified, UsageCount. n:m zu Content via ContentChapter (Composite PK ContentID+ChapterID).
  • Course (PK long ID): Name, Description, RoomNumber, StartDate/EndDate, InviteCode, SchoolId, CreatedByUserId, MapImageUrl. n:m zu Chapter via ChapterCourse; 1:n zu Registrierungen.

Beziehungen & Regeln (OnModelCreating)
#

  • StudentCourseRegistration (Composite PK StudentId+CourseId): Join Student↔Course. FK Course → DeleteBehavior.Cascade; FK Student → DeleteBehavior.Cascade. IsActive-Flag wird im Zugriffsmodell (StudentProxyContentsAsync) fĂŒr Proxy-Zugriffe genutzt.
  • LicensedContent (UserId+ContentID, Index): Benutzer-Lizenz; FK zu User DeleteBehavior.Cascade. Nur Lehrer haben Lizenzen.
  • Setting (Composite PK UserId+SettingKey): globale/benutzerbezogene Key-Value-Settings (z.B. AppTheme). MaxLength 128.
  • 1:1 ApplicationUser ↔ StudentProfile/TeacherProfile mit DeleteBehavior.Cascade; StudentProfile.SchoolId/TeacherProfile.SchoolId → DeleteBehavior.Restrict.
  • StudentProgress → User DeleteBehavior.Cascade.
  • Weitere EntitĂ€ten: StoreProduct (u.a. ProductType.Premium), Product, Asset, CoinTransaction, Invitation, School, ContentTag, SubscriptionStatus (DTO), TrophyGalleryItem.
  • StudentLimitExceededException: wird von der Paywall-PrĂŒfung bei voller KapazitĂ€t geworfen.
  • Reward / TrophyRequirement / RewardStatus: vollstĂ€ndig in CodingStandards.md spezifiziert, aber noch nicht als EntitĂ€t/DbSet implementiert (Design: bei TrophĂ€en-Erwerb prĂŒfen → Lehrer bestĂ€tigt “Belohnung gewĂ€hrt”).

5. Architektur & Patterns
#

Ebenen
#

  1. Client (WASM): Razor-Pages/Components rufen Client-Service-Wrapper auf (z.B. ContentService.cs, WalletService.cs, ClientStripeService.cs).
  2. HTTP API: Server-Controller (*Controller.cs in AptiGo/AptiGo/Services/) mit [Authorize] und REST-Endpunkten.
  3. Server-Dienste: DB*Service-Implementierungen hinter AptiGo.Shared.Interfaces.*-Interfaces (z.B. IContentService → DBContentService).
  4. Datenzugriff: ApplicationDbContext via IDbContextFactory<ApplicationDbContext> (wichtig fĂŒr Scoped-Services ohne Context-Sharing!).

Zentrale DI-Registrierungen (Program.cs)
#

  • Repository-Muster: IContentService→DbContentService, IChapterService→DbChapterService, ICourseService→DBCourseService, IPointsService→DBPointsService, IWalletService→DBWalletService, IStoreService→DBStoreService, IAssetService→DBAssetService, IAptiBoxService→DBAptiBoxService, ISchoolService→DBSchoolService, IProfileService→DBProfileService, IInvitationService→DBInvitationService, IContentReportService→DbContentReportService, ILicensingStateService→DBLicensingStateService.
  • IConfigService→ConfigManager (liest Setting-Tabelle), IAccessPolicy→AccessPolicy.
  • IEmailSender<ApplicationUser>→SmtpEmailSender (Transient), IPaymentService→StripePaymentService.
  • ISubscriptionService + ISubscriptionEnrollmentService → SubscriptionService; SubscriptionOptions via IConfiguration.GetSection("Subscription").
  • Background-Jobs: ContentMetadataJob, SubscriptionExpiryJob (AddHostedService).
  • Identity: AddIdentityCore<ApplicationUser>().AddRoles<IdentityRole<Guid>>().AddEntityFrameworkStores<ApplicationDbContext>() – Roles MUSS VOR AddEntityFrameworkStores registriert werden (Kommentar in Program.cs!) – SignIn.RequireConfirmedAccount=false, sehr schwache Passwortregeln (Entwicklung/Test).
  • MapRazorComponents<App>() mit AddAdditionalAssemblies(typeof(AptiGo.Client._Imports).Assembly).
  • DataSeeder: SeedRolesAsync beim Start.

Zugriffsmodelle (IAccessPolicy / AccessPolicy)
#

  • AllContents → alles (Admin).
  • MyContentsAsync → eigene + lizenzierte Content-IDs.
  • ShopContentsAsync → öffentlich (State Published) und nicht in “Mein Content”.
  • StudentProxyContentsAsync → Inhalte der Lehrer, bei denen der SchĂŒler aktiv (IsActive) eingeschrieben ist (inkl. deren Lizenzen).
  • GetAccessibleContentIdsAsync → Union (effiziente ZugriffsprĂŒfung).
  • Analog fĂŒr Chapters: MyChapterAsync / ShopChaptersAsync.

Controller (Auswahl)
#

AiController, AptiBoxController, AssetController, ChapterController, ConfigController, ContentController, ContentReportController, CoursesController, DemoController, LicensingController, MailController, PointsController, ProfileController, SchoolController, StoreController, StripePaymentController, SubscriptionController, WalletController.

Pages (AptiGo.Client/Pages)
#

Auth, ChapterBrowser, ChapterPicker, ChapterView, ComponentDisplay, ComponentDisplayV2, Counter, CourseEditor, CourseView, CreateContent, MailTest, SchoolDashboard, Shop, StudentChapterView, StudentChapterViewNew, StudentDashboard, TeacherDashboard, TestComponent, TrophyGallery.

Hinweis: Es existieren parallele Varianten (ComponentDisplay/ComponentDisplayV2, StudentChapterView/StudentChapterViewNew). Vor Änderungen prĂŒfen, welche aktiv verwendet wird.


6. Paywall / Subscription (Kern-Feature)
#

Aktuell intensiv in Entwicklung (Commits 68afacc „Paywall eingefĂŒhrt
", b9fdad4 „Defaultwert wird nun scheinbar korrekt ĂŒbernommen").

Konzept
#

  • Ein Lehrer hat ein Gesamt-Kontingent an SchĂŒlern ĂŒber alle Kurse (TeacherProfile.MaxStudents). Kostenloses Kontingent: 10 (Code-Default in SubscriptionDefaults.FreeTierMaxStudents) bzw. 40 laut appsettings.json (→ ⚠ Konflikt beachten!).
  • Premium-Tiers konfigurierbar unter appsettings.json → Subscription→Tiers:
    • premium-starter: 80 SchĂŒler, 30 Tage, ProductReferenceId 1000001
    • premium-pro: 160 SchĂŒler, 30 Tage, ProductReferenceId 1000002
  • Ein Tier ist nur gĂŒltig, wenn ein aktives StoreProduct (ProductType.Premium, State.Active) mit passender ReferenceID existiert (10 min MemoryCache-Validierung).
  • CurrentStudentsCount zĂ€hlt eingeschriebene SchĂŒler pro Kurs (in 2 Kursen ⇒ doppelt).

Service-Design (wichtig!)
#

  • ISubscriptionService (Shared): fern aufrufbar – GetSubscriptionStatusAsync, UpgradeSubscriptionAsync, CanEnrollStudentAsync. Öffnet eigene Contexts.
  • ISubscriptionEnrollmentService (Server): arbeitet ausschließlich auf dem vom Aufrufer ĂŒbergebenen DbContext, damit PrĂŒfung + Registrierung + ZĂ€hler in einer Transaktion laufen (thread-sicher). Methoden: AssertCanEnrollAsync(context, teacherId) (wirft StudentLimitExceededException), IncreaseEnrollmentCountAsync, DecreaseEnrollmentCountAsync, ApplyEnrollmentDeltaAsync, ReconcileAllCountsAsync.
  • Concurrency: TeacherProfile.RowVersion (xmin bei Postgres) + Retry (MaxEnrollmentRetries = 3).
  • Background-Jobs: SubscriptionExpiryJob (tĂ€glich 03:00 UTC, setzt abgelaufene zurĂŒck auf FreeTier), ReconcileAllCountsAsync als Drift-Ausgleich.
  • Frontend-Warnung im CourseEditor.razor wenn CurrentStudentsCount >= MaxStudents (Text laut CodingStandards) + Kick-Funktion pro Student.

Bekannte Unstimmigkeit
#

appsettings.json setzt Subscription.FreeTierMaxStudents = 40, der Code-Default in SubscriptionDefaults.FreeTierMaxStudents ist 10 – der Commit „Irgendwo sind 40 noch hardcodiert" deutet darauf hin, dass 40er-Werte noch hardcodiert auftauchen können. Beim Ändern immer beide Stellen prĂŒfen.


7. Content-System & ContentTypes
#

ContentType Enum (Werte): Text=1, Link=2, Media=3, Puzzle=4, Question=5, Trophy=6, MultipleChoice=7, MXInput=8, HotSpot=9, GapText=10, Tresor=11, Race=12, Correction=13, MDText=14 (+ Erweiterung möglich).

  • ContentState: Private | ShoolPublished | Published | Cloned | Archived | Sellable | Deleted. Cloned darf nicht veröffentlicht werden (Monetarisierung!). Deleted = Admin-Sperre.
  • DifficultyLevel: Förderniveau 
 Hauptstudienniveau (1–7).
  • ContentData-Konkrete in AptiGo.Shared/Models/ContentTypes/: TextContent, LinkContent, MediaContent, PuzzleContent, TrophyContent, AiQuestionContent, MultipleChoiceContent, NCalcContent (MXInput), HotspotContent, GapText, TresorContent, RaceContent, CorrecionContent (⚠ Tippfehler im Typnamen!), MDTextContent.
  • ContentActionContext(Points, ContentId, ChapterId, CourseId, CompletedIds) fĂŒr Punkte-/Fortschritts-Logik.
  • Editier-Pipeline (Razor-basiert): alte handler-basierte Render()/Edit()-Pfade wurden auf Razor-Komponenten umgestellt (ContentInjector + Typ-Editoren); bestehende Edit-UI (Speichern/Abbrechen, Publish-Checkbox, KI-Generierung, Meta-Felder) in ContentHost unbedingt erhalten.

8. KI-Integration
#

  • IAiContentService (Shared): KorrigiereAntwortAsync, GetAiInstruction, HelpStudentFromBot, TestCorrectionContentTask, GenerateMistakeTextTask.
  • Provider: Google (GoogleAiContentService, Client aus Google.GenAI, Singleton) und DeepSeek (DeepSeekAiContentService, AddHttpClient, Microsoft.Extensions.AI). Auswahl ĂŒber IAiServiceFactory (Config Settings.DefaultAiProvider, Default “DeepSeek”).
  • Modelle (Config): Google gemini-2.5-flash-lite, DeepSeek deepseek-chat.
  • Harte Regel: KI-Antworten mĂŒssen vor dem Übernehmen als strikt gĂŒltiges JSON fĂŒr den jeweiligen ContentType validiert werden (ContentData.IsValidJson) – sonst Vorgang abbrechen (Datenverlust vermeiden).
  • KI-Updates: nicht blind ersetzen/mergen – zwischen Append/Extend und Replace unterscheiden (Nutzer erwartet oft “ergĂ€nzen”).
  • IContentRunimeAware steuert runtime-aware Verhalten von Lernblöcken (z.B. eingebettete “Sidekick”-Hilfe, HelpBot).

9. Coding-Konventionen & Projektspezifische Regeln
#

Quellen: CodingStandards.md (Root) + .github/copilot-instructions.md.

Allgemein
#

  • Komponenten & Interfaces: PascalCase; Variablen/Methoden: camelCase.
  • Alle async Operationen mit try-catch.
  • Blazor: Datenbindung via @bind, EventCallback fĂŒr Eltern-Events, komplexe Logik in DI-Services (nicht in Komponenten).
  • NIE bestehenden, funktionierenden Code leichtfertig entfernen / mit NotImplementedException-Platzhaltern ersetzen – jede Zeile hat eine Funktion, GeschĂ€ftslogik vollstĂ€ndig bewahren.

Blazor / Rendering
#

  • Interaktive Seiten/Components brauchen explizit @rendermode InteractiveWebAssembly (sonst keine Interaktion).
  • Migration auf Razor-basierte Komponenten abgeschlossen/erwĂŒnscht; handler-basierte Render()/Edit()-Pfade entfernen.
  • Im Client werden User-IDs nie an den Client geschickt (Ausnahme: Debug).

Styling
#

  • AptiGo/AptiGo/wwwroot/AptiGo.css ist die zentrale Source of Truth fĂŒr app-weite Styles; globale UI-Regeln (z.B. HotSpot) dort zentral pflegen, nicht parallel in Komponenten-CSS duplizieren. (HotspotContentUi.razor.css wurde entfernt.)

Sicherheit & Assets
#

  • Angriffe auf fremde ChapterIDs im Editor → 403-Response statt Content-Fallback-Generierung.
  • Asset-AufrĂ€um/Sync liegt im DBAssetService (tĂ€gliche Sync-Methode, nur intern vom ContentMetadataJob genutzt); echte DeleteAsset löscht DB-Eintrag + Datei (mit API-Endpunkt).
  • AI-/HTML-Felder (Quill, KaTeX) vorsichtig behandeln; App.razor gibt bereits nichttemplatebare HTML-Ausgaben frei (KaTeX-Rendering via JS).

Gesperrte/Themenbezogene Regeln
#

  • BuyChapter: aktuell keine Implementierung – nur Routing-/Anzeige-Probleme lösen.
  • HelpBot: zentrale, low-overhead Lösung fĂŒr alle Contenttypen (keine Pro-Typ-Anpassungen).
  • TrophyGallery: Badge fĂŒr Anzahl direkt im TrophyGallery-UI ĂŒber der TrophĂ€e rendern (nicht im ContentHost).

10. Aktiver Kontext (Stand 11.08.2026)
#

Letzte Git-Historie (HEAD = master)
#

  • b9fdad4 – Defaultwert wird nun scheinbar korrekt ĂŒbernommen (Paywall-Default-Fix)
  • 68afacc – Paywall eingefĂŒhrt. Irgendwo sind noch 40 hardcodiert
  • 9e2dfcd – Mail jetzt ĂŒber SMTP direkt (Google-Account), SendGrid verworfen
  • b669e75 – Bugfix beim Veröffentlichen von Content
  • 5d6f02f – AI service gefixt (muss noch geprĂŒft werden)
  • 928edd3 – AIService klappt nicht
  • e2b6d8b – KaTeX im Markdown klappt
  • aaaf29f – Markdown in MediaUI und LinkContent

Offene/aktive Themen
#

  1. Paywall-Verifikation: FreeTier 40 vs. 10 Inkonsistenz; ggf. noch hardcodierte 40er-Werte.
  2. AI-Service: Provider-StabilitĂ€t (Google/DeepSeek) – „muss noch geprĂŒft werden".
  3. Reward-System: Design steht (CodingStandards.md), Implementierung offen (Status Pending/Ready/Confirmed, Gruppen-/Einzel-Rewards, Trigger bei TrophÀen).
  4. DualitĂ€t der Seiten: ComponentDisplay vs. V2, StudentChapterView vs. New – Verwendungsideal klĂ€ren.
  5. ContentHost-Edit-UI bleibt KernstĂŒck – bei Umbauten nicht beschĂ€digen.
  6. Footer-/Layout-Fix: Prerendering ist nicht Ursache; CSS/Layout (Viewport-Höhen, Overflow) prĂŒfen.
  7. HotSpot-Editor: Klickkoordinaten robust per JS-Interop im Vorschaubild ermitteln (keine MouseEventArgs-DOM-Annahmen); Styling zentral in AptiGo.css.

11. Build & Kommandos
#

  • Build: dotnet build AptiGo.slnx (bzw. Projektordner AptiGo/AptiGo).
  • Run (Server): dotnet run --project AptiGo/AptiGo/AptiGo.csproj
  • Migrationen: EF Tools (dotnet ef) – Migrationsordner: AptiGo/AptiGo/Migrations (Postgres). Aktuelle: PaywallSubscription.
  • DB lokal: PostgreSQL (Host=localhost;Database=aptigo_db;User=aptigo_user) laut appsettings.json; Legacy-SQLite aptigo.db ignoriert.
  • Keine Tests/CI-Pipelines aktuell vorhanden (.github/workflows leer).

12. Bekannte Fallstricke / Wichtig
#

  • appsettings.json ist gitignored (/AptiGo/AptiGo/appsettings.json) – Keys/Secrets (Stripe, Google, SMTP) liegen nur lokal. Nicht committen!
  • wwwroot/assets/ gitignored – Uploads (DBAssets) + Build-Assets nur lokal vorhanden.
  • Umlaute in JSON-ContentTypes: GapText, Tresor, Race, Correction, MDText nutzen _relaxedOptions (UnsafeRelaxedJsonEscaping).
  • CorrecionContent ist der tatsĂ€chliche Typname trotz Tippfehler – bei Refactorings Suche danach ausfĂŒhren.
  • opt-in Compound Keys: Settings/Paywall nutzen Composite-Keys; EF-Konfiguration in ApplicationDbContext.OnModelCreating zentral pflegen.
  • @rendermode ist pro Seite explizit zu setzen; Router nutzt AuthorizeRouteView.
  • Bei Concurrency-Fehlern (RowVersion) NICHT blind retrien; Fachlogik (SubscriptionService) gibt vor, wann ein Retry sinnvoll ist.