All Notes
Modularisierung von CDS Annotations
CAPCDSArchitectureBest Practices
Modularisierung von CDS Annotations
CDS Annotations sind das Herzstück von Fiori Elements Apps. Bei wachsender Komplexität wird eine strukturierte Organisation essentiell für die Wartbarkeit.
Problem
In kleinen Projekten werden alle Annotations in einer Datei definiert:
// ❌ Anti-Pattern: Alles in einer Datei (annotations.cds)
using TrackService from './track-service';
annotate TrackService.TimeEntries with @(
// UI Annotations (300 Zeilen)
UI.LineItem: [
{Value: workDate, Label: '{i18n>date}'},
{Value: startTime, Label: '{i18n>start}'},
// ...50 weitere Felder
],
UI.FieldGroup #Details: { /* ... */ },
UI.HeaderInfo: { /* ... */ },
// Common Annotations (50 Zeilen)
Common.Label: '{i18n>timeEntry}',
Common.Text: workDate,
// Capabilities (30 Zeilen)
Capabilities.InsertRestrictions: { /* ... */ },
Capabilities.UpdateRestrictions: { /* ... */ },
// Value Helps (80 Zeilen)
Common.ValueList #Project: { /* ... */ },
Common.ValueList #EntryType: { /* ... */ },
// Authorization (40 Zeilen)
@restrict: [ /* ... */ ],
// Field Controls (60 Zeilen)
@readonly, @mandatory, @UI.Hidden
// Analytics (30 Zeilen)
Analytics.Measure: true,
// Search (20 Zeilen)
Search.defaultSearchElement: true
);
// Weitere 5 Entitäten mit jeweils 200-500 Zeilen...Probleme:
- ❌ 600+ Zeilen pro Entität
- ❌ Schwer zu navigieren (Ctrl+F nötig)
- ❌ Merge-Konflikte häufig (mehrere Entwickler)
- ❌ Keine klare Trennung der Concerns
- ❌ Copy-Paste zwischen Entitäten
Lösung: Zwei-Ebenen-Struktur
Strukturierter Ansatz
srv/track-service/annotations/
├── annotations.cds # Master Import
├── common/ # Shared Concerns
│ ├── labels.cds # @Common.Text, Titles
│ ├── field-controls.cds # @readonly, @mandatory
│ ├── capabilities.cds # Insert/Update Restrictions
│ ├── value-helps.cds # @Common.ValueList
│ ├── authorization.cds # @restrict
│ └── search.cds # Search Configuration
└── ui/ # UI-Specific (Fiori Elements)
├── time-entries/
│ ├── list.cds # List Report
│ ├── object.cds # Object Page
│ ├── charts.cds # Charts & Analytics
│ └── actions.cds # Bound Actions UI
├── projects/
│ └── basic.cds # Simple CRUD
└── users/
└── basic.cds # Simple CRUD
Master Import (annotations.cds)
// 1. Common Concerns (entity-übergreifend)
using from './common/labels';
using from './common/field-controls';
using from './common/capabilities';
using from './common/value-helps';
using from './common/authorization';
using from './common/search';
// 2. UI-Specific (per entity)
using from './ui/time-entries/list';
using from './ui/time-entries/object';
using from './ui/time-entries/charts';
using from './ui/time-entries/actions';
using from './ui/projects/basic';
using from './ui/users/basic';Detaillierte Struktur
1. Labels (common/labels.cds)
Zweck: Texte, Übersetzungen, Titles
using TrackService from '../../track-service';
// TimeEntries
annotate TrackService.TimeEntries with @(
Common.Label: '{i18n>timeEntry}',
Common.Text: workDate,
Common.TextArrangement: #TextOnly
) {
workDate @(
Common.Label: '{i18n>workDate}',
Common.Text: workDate
);
entryType @(
Common.Label: '{i18n>entryType}',
Common.Text: entryType.text,
Common.TextArrangement: #TextFirst
);
project @(
Common.Label: '{i18n>project}',
Common.Text: project.name,
Common.TextArrangement: #TextOnly
);
}
// Weitere Entitäten...2. Field Controls (common/field-controls.cds)
Zweck: @readonly, @mandatory, @UI.Hidden
using TrackService from '../../track-service';
annotate TrackService.TimeEntries with {
// Read-only Felder (berechnet)
durationHoursGross @readonly;
durationHoursNet @readonly;
overtimeHours @readonly;
undertimeHours @readonly;
// Pflichtfelder
user @mandatory;
workDate @mandatory;
startTime @mandatory;
endTime @mandatory;
// Versteckte technische Felder
source @UI.Hidden;
createdAt @UI.Hidden;
modifiedAt @UI.Hidden;
}3. Capabilities (common/capabilities.cds)
Zweck: Insert/Update/Delete Restrictions
using TrackService from '../../track-service';
annotate TrackService.TimeEntries with @(
Capabilities: {
InsertRestrictions: {
Insertable: true,
RequiredProperties: [
user_ID,
workDate,
startTime,
endTime,
entryType_code
]
},
UpdateRestrictions: {
Updatable: true,
NonUpdatableNavigationProperties: [user]
},
DeleteRestrictions: {
Deletable: true
},
FilterRestrictions: {
FilterExpressionRestrictions: [{
Property: workDate,
AllowedExpressions: 'MultiValue'
}]
}
}
);4. Value Helps (common/value-helps.cds)
Zweck: Dropdowns, F4-Hilfen
using TrackService from '../../track-service';
annotate TrackService.TimeEntries with {
entryType @(
Common.ValueList: {
CollectionPath: 'EntryTypes',
Parameters: [
{ $Type: 'Common.ValueListParameterInOut',
LocalDataProperty: entryType_code,
ValueListProperty: 'code' },
{ $Type: 'Common.ValueListParameterDisplayOnly',
ValueListProperty: 'text' }
]
},
Common.ValueListWithFixedValues: true
);
project @(
Common.ValueList: {
CollectionPath: 'Projects',
Parameters: [
{ $Type: 'Common.ValueListParameterInOut',
LocalDataProperty: project_ID,
ValueListProperty: 'ID' },
{ $Type: 'Common.ValueListParameterDisplayOnly',
ValueListProperty: 'name' },
{ $Type: 'Common.ValueListParameterDisplayOnly',
ValueListProperty: 'number' }
]
},
Common.ValueListRelevantQualifiers: ['active']
);
}5. UI List Report (ui/time-entries/list.cds)
Zweck: List Report Table Configuration
using TrackService from '../../../track-service';
annotate TrackService.TimeEntries with @(
UI.SelectionFields: [
user_ID,
workDate,
entryType_code,
project_ID,
status_code
],
UI.LineItem: [
{ Value: workDate, Label: '{i18n>workDate}' },
{ Value: entryType.text, Label: '{i18n>type}' },
{ Value: project.name, Label: '{i18n>project}' },
{ Value: startTime, Label: '{i18n>start}' },
{ Value: endTime, Label: '{i18n>end}' },
{ Value: durationHoursNet, Label: '{i18n>netHours}',
Criticality: netHoursCriticality },
{ Value: overtimeHours, Label: '{i18n>overtime}',
Criticality: overtimeCriticality },
{ Value: status.name, Label: '{i18n>status}',
Criticality: statusCriticality },
{ $Type: 'UI.DataFieldForAction',
Action: 'TrackService.markDone',
Label: '{i18n>markDone}' }
],
UI.PresentationVariant: {
SortOrder: [{
Property: workDate,
Descending: true
}],
Visualizations: ['@UI.LineItem']
}
);6. UI Object Page (ui/time-entries/object.cds)
Zweck: Object Page Sections & Field Groups
using TrackService from '../../../track-service';
annotate TrackService.TimeEntries with @(
UI.HeaderInfo: {
TypeName: '{i18n>timeEntry}',
TypeNamePlural: '{i18n>timeEntries}',
Title: { Value: workDate },
Description: { Value: entryType.text }
},
UI.Identification: [
{ Value: workDate },
{ Value: entryType.text },
{ Value: status.name, Criticality: statusCriticality }
],
UI.Facets: [
{ $Type: 'UI.ReferenceFacet',
Label: '{i18n>generalInfo}',
Target: '@UI.FieldGroup#General' },
{ $Type: 'UI.ReferenceFacet',
Label: '{i18n>timeDetails}',
Target: '@UI.FieldGroup#TimeDetails' },
{ $Type: 'UI.ReferenceFacet',
Label: '{i18n>calculations}',
Target: '@UI.FieldGroup#Calculations' },
{ $Type: 'UI.ReferenceFacet',
Label: '{i18n>attachments}',
Target: 'attachments/@UI.LineItem' }
],
UI.FieldGroup #General: {
Data: [
{ Value: user.name },
{ Value: project.name },
{ Value: activity.text },
{ Value: workLocation.text },
{ Value: travelType.text }
]
},
UI.FieldGroup #TimeDetails: {
Data: [
{ Value: startTime },
{ Value: endTime },
{ Value: breakMin }
]
},
UI.FieldGroup #Calculations: {
Data: [
{ Value: durationHoursGross },
{ Value: durationHoursNet },
{ Value: overtimeHours, Criticality: overtimeCriticality },
{ Value: undertimeHours, Criticality: undertimeCriticality }
]
}
);Vorteile
1. Übersichtlichkeit
Vorher: 1 Datei × 2.500 Zeilen = ❌ unübersichtlich
Nachher: 15 Dateien × ~150 Zeilen = ✅ navigierbar
2. Klare Verantwortlichkeiten
Jede Datei hat einen Zweck:
labels.cds→ Textelist.cds→ List Reportobject.cds→ Object Page
3. Paralleles Arbeiten
👤 Developer A: Arbeitet an List Report (list.cds)
👤 Developer B: Arbeitet an Object Page (object.cds)
→ Keine Merge-Konflikte ✅
4. Wiederverwendung
// Shared Field Controls für alle Entitäten
using from './common/field-controls';
// Spezifische UI nur wo benötigt
using from './ui/time-entries/list';Best Practices
1. Naming Conventions
✅ Gut: labels.cds, list.cds, object.cds
❌ Schlecht: anno1.cds, stuff.cds, temp.cds
2. Ein Concern pro Datei
// ✅ Gut: Nur Labels
annotate TimeEntries with {
workDate @Common.Label: '{i18n>workDate}';
}
// ❌ Schlecht: Labels + UI gemischt
annotate TimeEntries with {
workDate @Common.Label: '{i18n>workDate}'
@UI.LineItem; // Falscher Concern!
}3. Master Import
// annotations.cds als zentrale Import-Datei
using from './common/labels';
using from './common/capabilities';
using from './ui/time-entries/list';4. Konsistente Struktur
Alle Entitäten folgen gleichem Muster:
ui/entity-name/
├── list.cds
├── object.cds
└── charts.cds (optional)
Implementierung in CAPture Time
Vollständige Struktur
srv/track-service/annotations/
├── annotations.cds # Master Import (20 Zeilen)
├── common/
│ ├── labels.cds # 150 Zeilen
│ ├── field-controls.cds # 80 Zeilen
│ ├── capabilities.cds # 120 Zeilen
│ ├── value-helps.cds # 200 Zeilen
│ ├── authorization.cds # 100 Zeilen
│ └── search.cds # 50 Zeilen
└── ui/
├── time-entries/
│ ├── list.cds # 150 Zeilen
│ ├── object.cds # 200 Zeilen
│ ├── charts.cds # 100 Zeilen
│ └── actions.cds # 80 Zeilen
├── projects/
│ └── basic.cds # 100 Zeilen
└── users/
└── basic.cds # 80 Zeilen
Gesamt: ~1.400 Zeilen in 14 Dateien
Statt: 1.400 Zeilen in 1 Datei ❌
Alternativen
Option 1: Monolith (1 Datei)
❌ Unübersichtlich ab 500+ Zeilen
Option 2: Pro Entität (1 Datei pro Entity)
⚠️ Besser, aber immer noch große Dateien
Option 3: Nach Concerns (gewählt)
✅ Maximale Modularität
Trade-offs
Vorteile:
- ✅ Übersichtliche Dateien (< 200 Zeilen)
- ✅ Klare Trennung der Concerns
- ✅ Paralleles Arbeiten ohne Konflikte
- ✅ Einfaches Finden von Annotations
Nachteile:
- ⚠️ Mehr Dateien (14 statt 1)
- ⚠️ Mehr Navigationsaufwand zwischen Dateien
- ⚠️ Initialer Setup-Aufwand
Wann modularisieren?
✅ Modularisieren wenn:
- Annotations-Datei > 300 Zeilen
- Mehrere Entwickler arbeiten an UI
- Mehrere Fiori Apps (List Report, Analytical List)
- Komplexe Object Pages mit vielen Facets
❌ Nicht modularisieren wenn:
- Nur 1-2 Entitäten
- Sehr einfache CRUD-UI
- Team < 2 Entwickler
- Annotations < 200 Zeilen
Fazit
Die Modularisierung nach Concerns ist essentiell für wartbare Fiori Elements Apps. Sie ermöglicht:
- Übersichtlichen Code durch kleine Dateien
- Paralleles Arbeiten ohne Merge-Konflikte
- Klare Verantwortlichkeiten pro Datei
- Einfache Navigation durch logische Struktur
Siehe auch
- Fiori Elements Documentation
- CDS Annotations Reference
- ADR 0006: CDS Annotations im Original-Projekt