Event Subscriber Best Practices in Business Central - typische Fehler, Performance & Reihenfolge
Ein typischer Projektstart:
„Wir machen es sauber - wir machen alles über Events."
Drei Monate später sieht die Realität so aus:
- Buchungen dauern 10 Sekunden
- Irgendwas triggert doppelt
- Job Queue crasht
- Und keiner weiß, woher es kommt
Der Developer ist ratlos.
Der Product Owner ist genervt.
Und das Management fragt: „Warum ist das System so langsam geworden?"
Und das Spannende ist:
Events sind das beste Erweiterungskonzept in Business Central - aber sie sind kein Freifahrtschein.
In diesem Artikel schauen wir uns an, warum Event Subscriber nicht automatisch „sauber" sind, welche typischen Fallen es gibt und wie man Subscriber wirklich strukturiert.
1) Warum Events in BC so wichtig sind (kurz)
Events sind das offizielle Erweiterungsmodell in Microsoft Dynamics 365 Business Central.
Der Grund ist einfach:
- Extensions dürfen Standard-Code nicht ändern
- Copy/Paste von Standardobjekten ist nicht erlaubt
- Events sind vom Standard vorgesehene Erweiterungspunkte
- Dadurch bleibt der Code upgrade-sicher
Das ist auch gut so - denn sonst wäre jede Extension ein potenzielles Update-Problem.
Aber: Events sind nicht automatisch „gut". Sie sind nur das Werkzeug.
2) Die 3 Event-Arten - und wann man welche nimmt
Bevor wir zu den Fehlern kommen, müssen wir die drei Event-Typen verstehen.
Integration Events
Das sind Events, die Microsoft (oder andere Extensions) explizit im Code platziert haben.
Beispiel:
[IntegrationEvent(false, false)]
local procedure OnBeforePostSalesDoc(var SalesHeader: Record "Sales Header")
begin
end;
Eigenschaften:
- Explizit gesetzt
- Meist stabil über Updates hinweg
- Dokumentiert (oder sollte es sein)
- Upgrade-sicher
Wann nutzen:
Immer, wenn es einen passenden Integration Event gibt.
Business Events
Das sind Events, die primär für externe Systeme gedacht sind (z. B. Power Automate, Logic Apps).
Sie sind weniger für AL-Subscriber gedacht, sondern für Event Grid / Webhooks.
Wann nutzen:
Für Integrationen außerhalb von BC.
Trigger Events (Table/Page Events)
Das sind Events, die automatisch von Table- und Page-Operations gefeuert werden:
OnAfterInsertEventOnBeforeModifyEventOnAfterDeleteEventOnAfterValidateEvent- etc.
Eigenschaften:
- Mächtig
- Aber: Performance-kritisch
- Können sehr oft gefeuert werden
- Keine explizite Dokumentation
Wann nutzen:
Nur wenn es keinen passenden Integration Event gibt - und mit Vorsicht.
3) Der #1 Anfängerfehler: Subscriber als Business-Logik-Mülleimer
Das ist einer der häufigsten Fehler in Business-Central-Projekten.
Entwickler bauen:
- Überall kleine Subscriber
- Überall kleine Sonderlogiken
- Überall direkte Datenbankzugriffe
Ergebnis: „Event Spaghetti"
Nach einem Jahr sieht das so aus:
- 50+ Subscriber Codeunits
- Niemand weiß, was wann triggert
- Debugging wird zur Hölle
- Performance bricht ein
Was ist das Problem?
Das Problem ist nicht der Event Subscriber selbst - sondern dass er als Container für Fachlogik missbraucht wird.
Anti-Pattern:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', false, false)]
local procedure OnBeforePostSalesDoc(var SalesHeader: Record "Sales Header")
var
Customer: Record Customer;
Item: Record Item;
begin
// 50 Zeilen Fachlogik direkt hier
Customer.Get(SalesHeader."Sell-to Customer No.");
if Customer."Customer Type" = Customer."Customer Type"::VIP then begin
// noch mehr Logik
end;
// noch mehr...
end;
Das ist nicht testbar, nicht wiederverwendbar und nicht wartbar.
Best Practice
Subscriber orchestrieren, Logik in Services auslagern:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', false, false)]
local procedure OnBeforePostSalesDoc(var SalesHeader: Record "Sales Header")
var
SalesValidationService: Codeunit "Sales Validation Service";
begin
SalesValidationService.ValidateVIPCustomer(SalesHeader);
end;
Die eigentliche Logik liegt in der Service Codeunit:
codeunit 50100 "Sales Validation Service"
{
procedure ValidateVIPCustomer(var SalesHeader: Record "Sales Header")
var
Customer: Record Customer;
begin
if not Customer.Get(SalesHeader."Sell-to Customer No.") then
exit;
if Customer."Customer Type" <> Customer."Customer Type"::VIP then
exit;
// Fachlogik hier
end;
}
Vorteile:
- Service ist testbar
- Logik ist wiederverwendbar
- Subscriber ist dünn und klar
4) Reihenfolge: Das Problem, das jeder irgendwann hat
Hier wird es richtig kritisch.
Viele Developer gehen davon aus:
„Mein Subscriber läuft zuerst."
Das ist falsch.
Die Wahrheit über Subscriber-Reihenfolge
Business Central garantiert keine Reihenfolge zwischen Subscribern.
Das bedeutet:
- Du kannst nicht davon ausgehen, dass Subscriber A vor Subscriber B läuft
- Andere Extensions können ebenfalls subscriben
- Updates können die (interne) Reihenfolge verändern
- AppSource-Apps können dazwischenfunken
Typisches Problem:
// Extension A
[EventSubscriber(..., 'OnBeforePostSalesDoc', ...)]
local procedure SetCustomField(var SalesHeader: Record "Sales Header")
begin
SalesHeader."Custom Field" := 'Value';
end;
// Extension B
[EventSubscriber(..., 'OnBeforePostSalesDoc', ...)]
local procedure UseCustomField(var SalesHeader: Record "Sales Header")
begin
if SalesHeader."Custom Field" <> 'Value' then
Error('Custom Field not set!');
end;
Das funktioniert manchmal - und manchmal nicht.
Warum ist das so gefährlich?
Weil:
- Es im Test funktioniert
- Es in Sandbox funktioniert
- Aber in Production plötzlich bricht (weil eine andere Extension dazugekommen ist)
Best Practice
Baue niemals Abhängigkeiten zwischen Subscribern.
Jeder Subscriber muss eigenständig funktionieren.
Wenn du Daten zwischen Subscribern teilen musst, nutze:
- SingleInstance Codeunits (als temporärer State-Container)
- Temporary Tables
- oder ändere die Architektur
5) Performance: Warum 1 Subscriber = ok, 50 Subscriber = Katastrophe
Jetzt wird es richtig praktisch.
Event Subscriber können Performance-Killer sein - wenn man nicht aufpasst.
Typische Performance-Fallen
Fall 1: FindSet() in jedem Subscriber
[EventSubscriber(ObjectType::Table, Database::"Sales Line", 'OnAfterModifyEvent', '', false, false)]
local procedure OnAfterModifySalesLine(var Rec: Record "Sales Line")
var
Item: Record Item;
begin
Item.SetRange("No.", Rec."No.");
if Item.FindSet() then // ← Falscher Query
repeat
// Logik
until Item.Next() = 0;
end;
Problem:
Dieser Subscriber wird pro Sales Line getriggert.
Bei 100 Zeilen = 100x FindSet().
Fall 2: Modify() ohne Grund
[EventSubscriber(...)]
local procedure UpdateSomething(var Rec: Record "Sales Header")
begin
Rec."Custom Field" := 'Value';
Rec.Modify(); // ← Triggert wieder Events!
end;
Das kann zu Endlosschleifen führen (siehe nächster Abschnitt).
Fall 3: Unnötige CalcFields()
[EventSubscriber(...)]
local procedure CheckBalance(var Customer: Record Customer)
begin
Customer.CalcFields(Balance, "Balance (LCY)"); // ← Pro Event!
if Customer.Balance > 10000 then
// Logik
end;
Problem:
CalcFields() macht SQL-Queries - und das pro Event-Aufruf.
Fall 4: COMMIT() im Subscriber
Das ist extrem gefährlich:
[EventSubscriber(...)]
local procedure LogSomething(var Rec: Record "Sales Header")
begin
// Logging
Commit(); // ← Bricht Transaktionen ab!
end;
Warum gefährlich?
- COMMIT beendet die aktuelle Transaktion
- Fehler danach können nicht mehr zurückgerollt werden
- Daten-Inkonsistenz ist die Folge
Nur in absoluten Ausnahmefällen nutzen.
Fall 5: Dialog/Message (GuiAllowed)
Wie in unserem GuiAllowed-Artikel erklärt:
[EventSubscriber(...)]
local procedure ShowMessage(var Rec: Record "Sales Header")
begin
Message('Something happened'); // ← Crasht in Job Queue!
end;
Immer prüfen:
if GuiAllowed then
Message('Something happened');
Die Faustregel
Ein Subscriber, der pro Sales Line 1x eine Tabelle liest, ist okay.
Ein Subscriber, der pro Sales Line 5 Tabellen scanned, killt jede Buchung.
6) Recursion / Endlosschleifen (der Klassiker)
Das ist einer der häufigsten Fehler überhaupt.
Das Problem
[EventSubscriber(ObjectType::Table, Database::"Sales Header", 'OnAfterModifyEvent', '', false, false)]
local procedure OnAfterModify(var Rec: Record "Sales Header")
begin
Rec."Last Modified By" := UserId;
Rec.Modify(); // ← Triggert OnAfterModifyEvent → Endlosschleife!
end;
Was passiert:
- User ändert Sales Header
- Subscriber wird getriggert
- Subscriber macht
Rec.Modify() - Das triggert wieder
OnAfterModifyEvent - Schleife beginnt
- Stack overflow / Performance-Tod
Wie verhindert man das?
Lösung 1: xRec vergleichen
[EventSubscriber(...)]
local procedure OnAfterModify(var Rec: Record "Sales Header"; var xRec: Record "Sales Header")
begin
if Rec."Last Modified By" = xRec."Last Modified By" then
exit; // Schon gesetzt → raus
Rec."Last Modified By" := UserId;
Rec.Modify();
end;
Lösung 2: Flag in SingleInstance Codeunit
codeunit 50110 "Recursion Guard"
{
SingleInstance = true;
var
IsRunning: Boolean;
procedure SetRunning()
begin
IsRunning := true;
end;
procedure ClearRunning()
begin
IsRunning := false;
end;
procedure IsAlreadyRunning(): Boolean
begin
exit(IsRunning);
end;
}
Dann im Subscriber:
[EventSubscriber(...)]
local procedure OnAfterModify(var Rec: Record "Sales Header")
var
Guard: Codeunit "Recursion Guard";
begin
if Guard.IsAlreadyRunning() then
exit;
Guard.SetRunning();
// Logik hier
Guard.ClearRunning();
end;
Lösung 3: RunTrigger := false (nur wenn sinnvoll)
Rec."Last Modified By" := UserId;
Rec.Modify(false); // RunTrigger = false
Aber Vorsicht:
Das deaktiviert alle Trigger - auch die, die du vielleicht brauchst.
7) „Subscriber feuert nicht" - die häufigsten Ursachen
Das ist ein sehr häufiges Troubleshooting-Szenario.
Checkliste: Warum feuert mein Subscriber nicht?
| Problem | Lösung |
|---|---|
| ✅ Falscher Event | OnBefore vs OnAfter verwechselt |
| ✅ Falsche Parameter-Signatur | Parameter-Namen müssen exakt passen |
| ✅ Event ist in anderer App | Dependency in app.json fehlt |
| ✅ Subscriber Codeunit nicht registriert | Codeunit muss in Extension sein |
| ✅ Event nur bei bestimmten Prozessen | z. B. nur bei Posting, nicht bei Release |
| ✅ Event ist conditional | Event wird nur unter bestimmten Bedingungen gefeuert |
| ✅ Scope falsch | OnPrem vs SaaS / Cloud |
Typisches Beispiel
Du subscribst auf:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', false, false)]
Aber der Event existiert als:
OnBeforePostSalesDocument
→ Namen stimmt nicht überein → Subscriber wird nie getriggert.
Debugging-Tipp
Setze einen Breakpoint im Subscriber und prüfe:
- Wird der Subscriber überhaupt getroffen?
- Wenn nein: Event-Name/Signatur prüfen
- Wenn ja, aber Logik läuft nicht: Bedingungen prüfen
8) Subscriber & Posting: Warum man hier besonders vorsichtig sein muss
Posting-Prozesse sind in Business Central extrem kritisch.
Warum ist Posting besonders?
- Performance-kritisch (oft hunderte Zeilen)
- Transaktions-kritisch (muss alles oder nichts sein)
- Fehler = Buchung bricht ab (User-Impact sofort sichtbar)
- COMMIT-Verhalten gefährlich (siehe oben)
Best Practices für Posting-Subscriber
✅ Keine UI
// ❌ Falsch
Message('Sales Order posted');
// ✅ Richtig
if GuiAllowed then
Message('Sales Order posted');
✅ Keine unnötigen Writes
Jedes Modify() / Insert() kostet Zeit und kann Fehler werfen.
✅ Logging statt Message
// Statt Message:
Session.LogMessage('SALES_POST', 'Order posted', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', 'SalesPosting');
✅ Klare Fehlertexte
Wenn dein Subscriber einen Fehler wirft, sollte klar sein, wo er herkommt:
Error('Custom validation failed in OnBeforePostSalesDoc: %1', DetailedMessage);
✅ Early Exit
Prüfe so früh wie möglich, ob der Subscriber überhaupt laufen muss:
[EventSubscriber(...)]
local procedure OnBeforePostSalesDoc(var SalesHeader: Record "Sales Header")
begin
if SalesHeader."Document Type" <> SalesHeader."Document Type"::Order then
exit; // Nicht für uns
if not SalesHeader."Custom Field" then
exit; // Feature nicht aktiv
// Jetzt erst die eigentliche Logik
end;
9) Best Practices (kompakt)
Hier meine wichtigsten Regeln für Event Subscriber:
✅ Subscriber dünn halten, Logik in Services auslagern
Subscriber = Orchestrierung
Services = Fachlogik
✅ Niemals auf Reihenfolge verlassen
Jeder Subscriber muss eigenständig funktionieren.
✅ Keine Datenbank-Schleifen in Line-Events
Ein FindSet() pro Sales Line = Performance-Tod.
✅ Recursion vermeiden (xRec + Guards)
xRec vergleichen oder SingleInstance Guard nutzen.
✅ Kein COMMIT im Subscriber
Nur in absoluten Ausnahmefällen - und dann dokumentieren warum.
✅ GuiAllowed nie voraussetzen
Siehe GuiAllowed-Artikel.
✅ Debugging/Telemetry einbauen
Session.LogMessage() nutzen, um Subscriber-Aufrufe nachzuvollziehen.
✅ Performance messen
Nutze BC Telemetry oder eigene Logging-Mechanismen, um langsame Subscriber zu identifizieren.
10) Bonus: So sieht ein guter Subscriber aus
Hier ein konkretes Beispiel:
codeunit 50120 "Sales Order Subscriber"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', false, false)]
local procedure OnBeforePostSalesDoc(var SalesHeader: Record "Sales Header"; var HideProgressWindow: Boolean)
var
SalesValidationService: Codeunit "Sales Validation Service";
Guard: Codeunit "Recursion Guard";
begin
// Early Exit: Feature nur für Orders
if SalesHeader."Document Type" <> SalesHeader."Document Type"::Order then
exit;
// Early Exit: Feature nicht aktiv
if not SalesHeader."Custom Validation Active" then
exit;
// Guard gegen Recursion
if Guard.IsAlreadyRunning() then
exit;
Guard.SetRunning();
// Eigentliche Logik in Service ausgelagert
SalesValidationService.ValidateCustomRules(SalesHeader);
Guard.ClearRunning();
end;
}
Was macht das Beispiel richtig?
- ✅ Early Exits → Performance
- ✅ Recursion Guard → Keine Endlosschleife
- ✅ Service Codeunit → Testbar & wiederverwendbar
- ✅ Keine DB-Calls → Schnell
- ✅ Keine UI → Funktioniert auch in Job Queue
- ✅ Klar strukturiert → Lesbar
Fazit
Event Subscriber sind der sauberste Weg, Business Central zu erweitern.
Aber sie skalieren nur dann gut, wenn man sie wie Architektur behandelt - nicht wie „Quick Fixes".
Die wichtigsten Erkenntnisse:
- Subscriber orchestrieren, Services enthalten Logik
- Reihenfolge ist nicht garantiert
- Performance ist kritisch bei Table Events
- Recursion ist der Klassiker
- Posting-Subscriber brauchen besondere Vorsicht
Wenn du diese Regeln befolgst, bleiben deine Extensions:
- Wartbar
- Performant
- Upgrade-sicher
- Und sie funktionieren auch dann noch, wenn andere Extensions dazukommen
Wenn dein System nur funktioniert, solange niemand eine zweite Extension installiert, ist es nicht fertig.