Platform — Common cases
Real situations when working with Platform, and how the system handles them. Useful for both engineering and support.
Add a new notification
First question: is this notification action-derived or event-derived?
Action-derived (direct):
var job = NotificationJobFactory.Create(eventTypeId: ..., eventData: ..., recipients: ...);
await _notificationClient.CreateNotificationJobAsync(_context, job);Event-derived id-only: attach notificationEventTypeId at the correct guarded call-site and remove the direct call. No new mapping is needed (HintedNotificationMapping handles the rest). Reference: AttendeeNewProfile — AttendeeService.CreateAttendeeAsync + ConsumerAccountController.
Event-derived complex data: write a dedicated IEntityEventNotificationMapping (such as SettingsUpdateNotificationMapping), build the job through the factory, and register DI.
No need to change
Handler/EntityEventNotificationPostProcessor. Rollback: remove the mapping registration inProgram.cs, or removenotificationEventTypeIdat the call-site.
"Customer receives two identical emails"
Common causes & system defenses:
| Cause | Defense |
|---|---|
| Queue redelivers event (retry) | Deterministic key: same event → same key → exactly one row (point-read dedup). |
| One notification type is wired to both event-derived and direct paths | Dual-path guardrail test: the two type sets must be disjoint; overlap → red test. |
| UI double-submit (direct flow without stable identity) | Intentionally at-least-once; defend at UI/request idempotency layer. |
If two emails really appear, check (1) whether that type is created by both paths, (2) whether the key is truly deterministic, and (3) whether the call-site double-writes.
"Mapping a generic CRUD event causes over-fire / spam"
⚠️ This is the biggest trap. Events such as Create_Attendee, Update_Employee fire from many code paths (including audit/history paths). Mapping them directly to notification will fire much more broadly than the current call-site, which may have guards such as isEdit or "invitation path".
Correct approach: do not map the generic CRUD event. Instead, attach a hint (NotificationEventTypeId) at the exact guarded call-site. Events without a hint are skipped ⇒ no over-fire.
Real-world example: You want to send a "new child profile" email; a developer maps
Create_Attendeedirectly. → Avoid sending from import/audit paths, not just real registration. → Attach a hint at the guarded call-site (AttendeeNewProfile) → only that path fires; other paths are skipped.
"Retry creates duplicate history rows"
Previously history wrote with RowKey = Guid.NewGuid() → retry created a second row. Now history goes through EntityEventHistoryPostProcessor with deterministic HistoryEventKey + insert-or-replace:
- Retry same event → same key → overwrites the same row (no duplicate).
- Two genuinely different events → different
CreatedOn→ different rows (history preserved).
HistoryEventKey shape:
entity-history:{EventTypeId}:{EntityTypeId}:{entityIdentity}:{CreatedOn:O}:{ActionTypeId}entityIdentity prioritizes EntityGuid → EntityId → BusinessUnitId for stability across message formats.
Add a new History Rule
- Write an
IEntityEventHistoryMapping(orHintedHistoryMappingfor snapshot + derived action, or multi-mapping for one-to-many). Build onlyAddEntityToHistoryModel— leaveRowKeyempty. - Register it in worker DI. The post-processor does not need to change.
- Remove only the history branch from that domain's
switch— keepAddSchedulerAsync/ projection.
A fully new concern (automation, reminder, etc.) should be added as a new
IEntityEventPostProcessorwith its own registry — do not bloat the routing handler or branch inside the existing post-processor.
"New notification does not send although job was created"
The send pipeline evaluates Notification Settings at the Scheduler step. Possible causes:
- Notification type has not been seeded into
NotificationOptionfor the BU (for example,IncidentSubmittedneedsscripts/seed_incident_submitted_notification.sql). - BU has no configured subscriber/recipient → handler skips gracefully.
- Generator has no handler for the new type (new feature) → wire the route in Generator.
"Need to know which type, how many, through which worker"
EntityEventNotificationPostProcessor logs one structured line each time it creates an event-derived job (EventTypeId / EventKey / BusinessUnitId). Use that log to count. Aggregate metrics/dashboard are a follow-up (requires a metrics system).
What does NOT change (safe during refactor)
Scheduler/Generator/Distributor send pipeline and delivery; template rendering; EntityHistory schema + read path; Finance Transaction Projection pipeline; and numeric ids for EntityType / EventType / EntityActionType / NotificationEventType. Platform refactors only change where side effects are created and how uniqueness is preserved — not what downstream sees.