CAF — wiederverwendbare Basis

Basisdokumentation (src/Modules) · Stand 29.08.2026 11:32
3Kapitel
2193Wörter
~10 Min.Lesezeit
4 Bausteinesrc/Modules

01. Zweck und Abgrenzung

Zweck

CAF ist die wiederverwendbare Basis unter src/Modules/: anwendungsneutrale ABP-Bausteine, die ein Produkt einbindet, statt sie je Produkt neu zu schreiben. Sie sind Quellcode-Bausteine, keine NuGet-Pakete (IsPackable=false) und werden per ProjectReference konsumiert; verbindlich ist je Baustein die Vertragsdatei VERTRAG.md (ADR-03).

Heute existiert genau ein konsumierendes Produkt, das RAL Gütezeichen Anhörungsportal. Es ist gleichzeitig Referenzhost: Was dort funktioniert, gilt als belegt; was dort nicht gebraucht wird, wird hier nicht vorsorglich gebaut.

Die Grenze

Die tragende Regel ist einseitig und wird nicht durch Konvention, sondern durch Architekturtests durchgesetzt (ADR-06):

Ein Produkt darf von CAF abhängen. CAF darf nie von einem Produkt abhängen.

Konkret darf kein CAF.Modules.*-Projekt ein CAF.Applications.*-Projekt oder -Paket referenzieren, in keiner Version, und keinen produktfachlichen Namespace oder Begriff verwenden. Eine Verletzung ist ein Build-Fehler, kein Review-Befund.

Entscheidungsregel: gehört etwas nach CAF?

Ein Belang gehört nur dann in einen CAF-Baustein, wenn alle Punkte zutreffen:

  1. Domänenneutral — er lässt sich ohne produktfachliche Begriffe beschreiben. DTOs, Permissions, Routen und Fachbegriffe eines Produkts bleiben beim Produkt.
  2. Belegter Bedarf — es gibt einen konkreten, getesteten Anwendungsfall. Eine vorsorgliche Abstraktion ohne Konsumenten ist ausdrücklich kein Grund.
  3. Stabil zusicherbar — die Fläche lässt sich in VERTRAG.md benennen und nach SemVer versionieren.

Trifft einer der Punkte nicht zu, bleibt der Belang im Produkt. Der Umzug nach CAF ist später mechanisch möglich, der Rückweg nicht.

Was CAF ausdrücklich nicht ist

Aus den Verträgen der Bausteine, nicht nachträglich interpretiert:

  • Kein Policy-Owner. Rollen, Permissions, Realm-Namen und Autorisierungsentscheidungen bleiben beim Produkt.
  • Kein Owner von Routing, Layout oder Render Mode. Der Host entscheidet.
  • Keine Benutzerverwaltung und kein Provisioning. CAF.Modules.Authentication ist ein OIDC-Client, keine Identitätsverwaltung.
  • Keine zweite Wrapper-API über Fremdbibliotheken. CAF.Modules.UI.Blazor integriert Cohesion, es kapselt es nicht in eine eigene Komponenten-API.
  • Kein Sammelbecken. Ein Baustein Common oder Utils ist keine gültige Antwort auf die Ownership-Frage.

Wann ein Baustein aus CAF herausgelöst wird

Extraktionsfähigkeit ist eine Eigenschaft des Abhängigkeitsgraphen, kein Grund, heute Projekte anzulegen. Ein zweites konsumierendes Produkt ist der Beleg, der eine weitere Assembly- oder Paketgrenze rechtfertigt — nicht die Vermutung, dass es eines geben wird.

Weiter: 02. Bausteine · verbindliche Arbeitsregeln: src/Modules/AGENTS.md

02. Bausteine

Vier Bausteine unter src/Modules/, alle bewusst nicht packbar (IsPackable=false, keine PackageId/Version) und je mit einer verbindlichen VERTRAG.md als Quelltext-Vertrag. Der Referenzhost konsumiert sie per ProjectReference; ein Pack-Skript existiert nicht.

Baustein Verantwortung Grenze
Authentication OIDC-Client: Authorization Code + PKCE gegen einen externen Identity Provider (referenzgetestet mit Keycloak), Claim-Mapping, Cookie-/Session-Policy, lokale und remote Abmeldung, Security-Stamp-Revalidierung keine Produktrollen, Permissions oder Realm-Policy; keine Benutzerverwaltung; kein Provisioning
Documents.Contracts DocumentId, vendor-neutrale DTOs, Dokument-/Content-/Share-Interfaces, Permissions, Localization exportiert keine Volo.FileManagement-Typen
Documents (Runtime) dünner Adapter über ABP File Management: validierter Dateiinhalt, Metadatenzugriff, ablaufende und einzeln widerrufbare Direktfreigaben; besitzt nur CAFDocumentShareGrants ABP besitzt Descriptor und BLOB-Inhalt; keine parallelen Dokumententities
UI.Blazor Cohesion-Integration: AddCohesion, typisierte CAFCohesionOptions, frühe Lizenzvalidierung, Bundle-Contributors für CSS/JS, routenfreies CAFCohesionSmokePanel keine Produktroute, keine Fachkomponente, kein Render-Mode-Entscheid; keine Cohesion-Fassade (siehe unten)

Die zugesicherte Fläche und die ausdrücklichen Nicht-Zusicherungen je Baustein stehen in dessen VERTRAG.md — verlinkt in der Übersicht. Dieses Kapitel fasst nur zusammen und ersetzt keinen Vertrag.

Persistierte Kennungen

CAF-Bausteine behalten das Präfix CAF in persistierten Bezeichnern (CAFDocumentShareGrants), unabhängig davon, welches Präfix das konsumierende Produkt verwendet. Der Präfix markiert Ownership, nicht Technologie: Beim Lesen einer Tabelle ist ohne Blick in den Quellcode erkennbar, wer sie besitzt — und es fällt auf, wenn ein wiederverwendbarer Baustein unbemerkt produktspezifisches Wissen aufnimmt.

Die Cohesion-Referenz-Ausnahme

Cohesion (Syncfusion-basiert) ist die verbindliche Komponentenbibliothek für jede von CAF oder einem CAF-Produkt erstellte Oberfläche. MudBlazor, rohes Syncfusion und Blazorise dürfen von Fachcode nicht direkt gerendert werden; ausgenommen sind die von ABP gelieferten Administrations-/Account-/LeptonX-Seiten auf ihrem unterstützten MudBlazor-Unterbau.

Dabei gilt eine bewusste Ausnahme von der Regel „Fremdpakete nur über den zuständigen CAF-Baustein":

Jede Assembly, die Cohesion-Komponenten kompiliert, referenziert das Cohesion-Paket direkt per PackageReference — nicht über CAF.Modules.UI.Blazor.

Grund: Razor-Assets wirken zur Compile-Zeit (Komponenten-/TagHelper-Discovery, _Imports, Scoped CSS, Static Web Assets). Sie werden nicht verlässlich über eine ProjectReference-Kette reexportiert, anders als eine gewöhnliche Abhängigkeit auf eine kompilierte DLL. Ohne direkte Referenz bliebe ein Cohesion-Tag unaufgelöst und würde als leeres HTML-Element gerendert; die UI-Assemblies erzwingen deshalb RZ10012 als Fehler (WarningsAsErrors), womit der Build statt der Oberfläche bricht.

Daraus folgt die Rollenteilung:

  • CAF.Modules.UI.Blazor ist Runtime-Integration — Registrierung, Lizenzierung, Bundles, Policy — und wird ausschließlich vom Host eingebunden.
  • Razor-UI-Assemblies (Produkt-Shell, Feature-*.Blazor, und CAF.Modules.UI.Blazor selbst) referenzieren Cohesion direkt. Die Version wird zentral an einer Stelle festgelegt (Directory.Packages.props: Cohesion 3.*-*); die Projekte nennen keine eigene Version und kein VersionOverride — sonst lägen zwei Cohesion-Assemblies im selben Render-Baum. 3.*-* ist eine Floating-Version: sie löst die höchste verfügbare 3.x inklusive Prereleases auf, weil Cohesion mitentwickelt wird; ein Major-Wechsel bleibt eine bewusste Entscheidung. Bewusst kein Bereich wie [3.6.2-alpha,4.0.0): bei einem Bereich löst NuGet die niedrigste passende Version auf, die Abhängigkeit würde also still auf der Untergrenze festfrieren — und eine Untergrenze, die es auf dem Feed nicht wörtlich gibt, löst zusätzlich NU1603 aus (hier ein Fehler, weil Warnungen Fehler sind).

Bewusst in Kauf genommen: Zwei Restores desselben Commits können unterschiedliche Cohesion-Versionen auflösen. Das ist kein Versäumnis, sondern der Zweck — solange Cohesion mitentwickelt wird, ist dieser Build ein fortlaufender Integrationstest dagegen. Ein Lockfile würde eine Regression nicht verhindern, sondern ihre Entdeckung verschieben. Was es dafür braucht, ist Sichtbarkeit: die aufgelöste Version muss bei einem roten Build ablesbar sein. - Ein browser-wasm-Projekt darf das serverseitige Integrationsprojekt nicht referenzieren.

Diese direkte Referenz ist keine aufzuräumende Redundanz. Sie gegen CAF.Modules.UI.Blazor zu tauschen, bricht die Razor-Kompilierung.

Cohesion.Components wird global importiert, Cohesion.Components.Layout bewusst nicht — generische Namen wie Column kollidieren sonst.

Kommerzielle Abhängigkeiten

Zwei Bausteine sind an kommerzielle Pakete gekoppelt, was für einen externen Konsumenten Feed-Voraussetzungen schafft:

  • AuthenticationVolo.Abp.Account.Pro.Public.Web (lizenzierter ABP-Feed). Ein Architekturtest pinnt die Invariante „exakt einmal referenziert, exakt ein Vendor-DependsOn".
  • UI.BlazorCohesion aus dem GitLab-Gruppen-Feed (Quelle cohesion in NuGet.Config). Solange die aufgelöste Version ein Prerelease ist, bleibt auch der Baustein Prerelease-versioniert.

Cohesion nachschlagen statt raten

Cohesion liefert seinen Komponentenvertrag maschinenlesbar mit. Regel: Parameter werden nachgeschlagen, bevor Markup entsteht — nicht aus dem Gedächtnis rekonstruiert und nicht aus Syncfusion-Wissen abgeleitet. Cohesion besitzt seine API selbst; kein Syncfusion-Typ liegt auf einem öffentlichen Parameter, geraten wird also verlässlich falsch.

Zwei Wege, in dieser Reihenfolge:

  1. MCP-Server (Primärweg). Registriert in .mcp.json im Repository-Root (Server cohesion, node tools/cohesion-mcp/dist/index.js aus dem Cohesion-Checkout). Er beantwortet Komponenten-, Parameter-, Token- und Pattern-Fragen direkt.
  2. aidx/-Verzeichnis im NuGet-Cache (Offline-Weg). Das Paket bringt den Vertrag selbst mit, auch ohne Cohesion-Checkout und ohne laufenden Server:

text %USERPROFILE%\.nuget\packages\cohesion\<aufgelöste Version>\aidx\

Heute also …\cohesion\3.6.2-alpha.0.2\aidx\ — die aufgelöste Version liefert Get-ResolvedCohesionVersion bzw. der Statusbericht von setup.cmd -Verify. Inhalt:

Datei Zweck
llms.txt Kurzüberblick, Einstiegspunkt
llms-full.txt vollständiger Kontext zum einmaligen Laden
cohesion-catalog.json der Katalog: 116 Komponenten mit Kategorie, Basisklasse und vollständiger Parameterliste
snippets/*.md 16 Familien-Dateien (plus _TEMPLATE.md) mit je einem minimalen Beispiel

Zwei Fallen: llms.txt verweist intern auf docs/ai-dx/…, im NuGet-Paket liegt dieser Inhalt flach unter aidx/ — der genannte Pfad existiert im Paket nicht. Und der MCP-Server liest seinen eigenen Katalog aus dem Cohesion-Checkout; er kann damit von der restaurierten Paketversion abweichen. Versionsgenau zum Build ist nur aidx/.

Der Katalog entscheidet, welche Komponenten existieren — nicht die Erinnerung an eine frühere Version. In 3.6.2 sind CohesionSelect und CohesionSearchInput nicht enthalten; der Ersatz für eine Auswahlliste heißt CohesionDropDownList. Wer solche Namen ungeprüft schreibt, erzeugt Razor-Fehler statt UI.

Override-Register

Jeder lokale Behelf für eine Cohesion-Lücke steht hier mit der Gegenleistung, die ihn zurücknimmt. Ohne diese Spalte wird ein Workaround zum Dauerzustand, weil niemand mehr weiß, worauf er wartete.

Override Wo Wird zurückgenommen durch
var(--brand, #fe5000) — Marken-Orange als CSS-Fallback hart im Produkt-CSS (5 Stellen) Features/Hearings/Blazor/Pages/: HearingDashboardPage.razor.css (1), HearingDetailPage.razor.css (3), HearingRepliesPage.razor.css (1) Nutzung des mitgelieferten Themes cohesion-orange-*: dort ist --coh-color-primary = #fe5000 (verifiziert in staticwebassets/css/cohesion-orange-light.css). Danach ersetzt var(--coh-color-primary) den Fallback.
var(--brand-ultra-light, #fdf1e9) — aufgehellter Marken-Ton hart im Produkt-CSS (27 Stellen, 14 Dateien) Scoped CSS unter Features/Hearings/Blazor/: Pages/ (13 Dateien, u. a. HearingCreatePage, HearingArchivePage, HearingSettingsPage, HearingSubmitPage, HearingDetailPage) und Portal/ParticipationPortalPage.razor.css Kein Token trägt #fdf1e9 wörtlich. Das Theme bildet Tönungen als color-mix(in srgb, var(--coh-color-primary) N%, …) (z. B. --coh-grid-selection-bg 12 %). Zurückgenommen durch eine Token-Entscheidung beim Theme-Rollout, nicht durch simples Ersetzen.
Native <select>-Bridges statt Cohesion-Auswahlliste (29 Stellen, 10 Dateien) Features/Hearings/Blazor/Pages/ (20), Features/Institutions/Blazor/Pages/ (5), Features/Authorization/Blazor/Pages/StaffAccountsPage.razor (3), Features/Hearings/Blazor/Portal/MyParticipationsPage.razor (1) — 3 davon mit Marker-Kommentar „CohesionSelect entfiel" Migration auf CohesionDropDownList (Items/TextField/ValueField). Die Komponente existiert in 3.6.2, der Behelf ist also nur noch Migrationsarbeit, keine Lücke.
Produkt-Stylesheet überschreibt LeptonX' html/body-Bemalung per Spezifität Host/wwwroot/css/hearing-portal-chrome.css, gebündelt über HearingPortalModule.ConfigureBundles Wegfall des LeptonX-Themes, wenn die ABP-Adminseiten ihren Vendor-Stack verlassen (Plan: „Deliberat zuletzt"). Kein Cohesion-Mangel: Cohesion fasst html/body bewusst nicht an — der Override richtet sich gegen LeptonX, nicht gegen Cohesion, und ist deshalb an dessen Lebensdauer gebunden, nicht an eine Cohesion-Version.
Markenfarbe als Textfarbe auf --coh-color-primary abgedunkelt (Sidebar-Nav-Label aktiv/Gruppe, Footer-Links) derselbe Host-Stylesheet, color-mix(in srgb, var(--coh-color-primary) 65%, var(--coh-surface-fg)) Upstream-Korrektur der Cohesion-Paletten: sie verwenden --coh-color-primary unverändert als Textfarbe, was auf hellen Flächen 2,82–2,86:1 ergibt (WCAG AA verlangt 4,5:1) — gemessen im --audit-Lauf, nicht geschätzt. Sobald die Paletten eine eigene Textrolle für die Primärfarbe führen, entfällt dieser Block. Cohesion wird hier mitentwickelt, der Fix gehört also dorthin; bis dahin ist er im Produkt, weil eine Barrierefreiheitsverletzung nicht auf einen Paketwechsel warten kann.

Zu --brand und --brand-ultra-light: beide sind im Repository nirgends definiert. Der „Fallback" ist damit immer der wirksame Wert — der Hex-Wert ist der Override, nicht seine Absicherung.

Weitere aktuelle Overrides gegenüber Cohesion 3.6.2 wurden nicht gefunden; die vier Zeilen sind der vollständige Stand. Vier Overrides des zugrunde liegenden Playbooks sind in 3.6.2 bereits upstream zurückgenommen und stehen deshalb nicht hier.

Dieses Register wird bei jedem Cohesion-Versionswechsel neu gelesen, denn genau dort nimmt ein Upstream-Fix einen Behelf zurück — und ein Behelf, der nach dem Fix bleibt, ist ab dann ein Bug.

Weiter: 03. Erweiterung durch ein Produkt

03. Erweiterung durch ein Produkt

Wie ein Produkt CAF konsumiert — beschrieben am einzigen heute existierenden Konsumenten, dem RAL Gütezeichen Anhörungsportal. Andere Hosts können denselben Modulvertrag übernehmen, ohne Produktrouten oder Berechtigungen zu kopieren.

Einbindung

Ein Baustein wird über die ABP-Modulkette eingebunden, nicht über direkte Typreferenzen:

  1. ProjectReference (bzw. PackageReference außerhalb dieses Repositorys) vom Host auf den Baustein.
  2. Die ABP-Modulklasse des Bausteins in die DependsOn-Liste des Hostmoduls aufnehmen, z. B. typeof(CAFFrameworkUiBlazorModule).
  3. Den typisierten Optionsbaum des Bausteins über die Konfiguration des Hosts füllen. Die Optionen validieren beim Start und brechen fail-fast ab, wenn Pflichtwerte fehlen.

Der Host ist Composition Root: Er lädt Module, verkabelt Middleware und Routing, besitzt den gemeinsamen DbContext und erzeugt Migrationen — aber keine eigene Fachlogik und keine neuen Fachentities.

Was das Produkt beitragen muss

CAF sichert bewusst nicht zu, was produktspezifisch ist. Das Produkt bringt mit:

Belang Warum beim Produkt
Rollen, Permissions, Grants Autorisierungsentscheidungen sind fachlich, nicht neutral
Realm-/Mandantennamen, Client-IDs umgebungs- und produktspezifisch
Routing, Layout, Render-Mode-Entscheid der Host entscheidet zentral
Benutzerverwaltung und Provisioning Authentication ist reiner OIDC-Client
Fachliche Bedeutung von Dokumenten Documents liefert nur den neutralen Vertrag; Sichtbarkeit, Unveränderlichkeit und Download-Autorisierung trägt das Produkt
Globale Bundles des Hosts UI.Blazor liefert Contributors, der Host besitzt das Bundle

Erweiterungspunkte

Belegt und heute genutzt:

  • Typisierte Options mit Startvalidierung — jeder Baustein exponiert einen Optionsbaum, den der Host füllt. Standardmäßig deaktivierte Pfade (z. B. Cohesion ohne Lizenzschlüssel) verhindern, dass Tests oder Hilfshosts versehentlich etwas Unlizenziertes registrieren.
  • ABP-Bundle-Contributors — der Baustein liefert benannte Contributors, der Host fügt jeden genau einmal dem tatsächlich verwendeten globalen Bundle hinzu. Doppelte Registrierung lädt Assets zweimal.
  • Opt-in-Assets — bewusst nicht im Core enthaltene Assets (Tailwind Preflight, Font Awesome, Cohesion-Themes) ergänzt der Host explizit nach dem Core-Contributor, weil sie LeptonX-/MudBlazor-Styles zurücksetzen oder überschreiben können.
  • Routen- und render-mode-freie Komponenten — der Baustein liefert Komponenten ohne Route und ohne @rendermode, damit der Host Autorisierung und Interaktivität steuert.
  • Permission-freie PortsIDocumentContentPort prüft bewusst keine Berechtigung. Der aufrufende Use Case des Produkts muss den Objektzugriff vollständig geprüft haben, bevor er den Port aufruft. Eine ID, URL oder ein Dateiname verleiht selbst nie Zugriff.
  • Von außen implementierte Ports — braucht ein Baustein eine Auskunft, die nur das Produkt geben kann, wird sie als Port deklariert und vom Produkt implementiert, statt eine Rückabhängigkeit zu erzeugen.

Verträge ändern

Wer die zugesicherte Fläche eines Bausteins ändert, aktualisiert dessen VERTRAG.md im selben Änderungssatz. Die dort dokumentierte SemVer-Regel (brechend nur mit Major, additiv mit Minor, Korrekturen mit Patch) beschreibt die Absicht für den Fall einer späteren Paketierung — heute gibt es keine Paketversion, weil die Bausteine nicht packbar sind. FrameworkPackagingTests pinnt beides: die Nicht-Packbarkeit (keine PackageId, keine Version, kein PackageReadmeFile) und dass jede VERTRAG.md existiert, nicht leer ist und ihre zwei Pflichtabschnitte trägt.

Offen

Der Pfad für einen Konsumenten außerhalb dieses Repositorys existiert nicht: Die Bausteine sind bewusst nicht packbar, es gibt also keinen Feed-, Versions- und Upgradeweg. Ein zweites Produkt im selben Repository konsumiert sie per ProjectReference und braucht das auch nicht. Siehe Bekannte Lücken.

Zurück: 01. Zweck und Abgrenzung · 02. Bausteine