Replace shared email commands with MediatR notifications (#280)

This commit is contained in:
Aaron Po
2026-09-05 22:37:00 -04:00
committed by GitHub
parent 4aa7ba5d9b
commit d958933250
24 changed files with 322 additions and 218 deletions

View File

@@ -77,7 +77,7 @@ flowchart TB
subgraph SHARED["Shared Projects"]
direction LR
SH1["Shared.Contracts<br/>ResponseBody envelope"]
SH2["Shared.Application<br/>ValidationBehavior,<br/>cross-slice email commands"]
SH2["Shared.Application<br/>ValidationBehavior"]
end
subgraph DOMAIN["Domain Layer"]
@@ -98,7 +98,7 @@ flowchart TB
DB[("Database (SQL Server)")]
HOST -- "discovers controllers via<br/>AddApplicationPart" --> SLICES
S1 -- "SendRegistrationEmailCommand<br/>(via Shared.Application contract)" --> S3
S3 == "ProjectReference<br/>(handles UserRegisteredNotification & ConfirmationEmailResendRequestedNotification)" ==> S1
S1 -- "ProjectReference<br/>(avatar upload)" --> S5
SLICES --> SHARED
@@ -172,8 +172,10 @@ brewery photo upload command) rather than bound to an HTTP route directly.
`Infrastructure.Email.Templates` for Emails, `Infrastructure.FileUpload` for
PhotoUpload)
- `Shared.Contracts`, `Shared.Application`
- No other `Features.*` project, with one exception: `Features.Users` references
`Features.PhotoUpload` directly for avatar uploads
- No other `Features.*` project, with two exceptions: `Features.Users`
references `Features.PhotoUpload` directly for avatar uploads, and
`Features.Emails` references `Features.Users` directly to handle
`UserRegisteredNotification` and `ConfirmationEmailResendRequestedNotification`
**Rules**:
@@ -191,7 +193,7 @@ needs it, or because duplicating it four times would be worse than sharing it
| Project | Purpose |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Shared.Contracts` | `ResponseBody<T>`/`ResponseBody`, the API response envelope every controller returns |
| `Shared.Application` | `ValidationBehavior<TRequest,TResponse>` (the MediatR pipeline behavior that runs FluentValidation before a handler executes) and the cross-slice email commands (`SendRegistrationEmailCommand`, `SendResendConfirmationEmailCommand`) |
| `Shared.Application` | `ValidationBehavior<TRequest,TResponse>`, the MediatR pipeline behavior that runs FluentValidation before a handler executes |
**Rules**:

View File

@@ -127,9 +127,9 @@ sequenceDiagram
TokenSvc-->>RegHandler: Confirmation Token
deactivate TokenSvc
RegHandler->>Mediator: Send(SendRegistrationEmailCommand) (Shared.Application cross-slice contract)
RegHandler->>Mediator: Publish(UserRegisteredNotification)
activate Mediator
Note right of Mediator: Routed to Features.Emails' SendRegistrationEmailHandler — the second of two cross-slice dispatches in this flow (profile creation is the first)
Note right of Mediator: Features.Emails has a direct ProjectReference on Features.Users and handles the notification via UserRegisteredNotificationHandler — the second of two cross-slice dispatches in this flow (profile creation is the first)
Mediator->>EmailDispatcher: SendRegistrationEmailAsync(firstName, email, confirmationToken)
activate EmailDispatcher
@@ -147,15 +147,15 @@ sequenceDiagram
SMTP-->>EmailDispatcher: Success / throws
deactivate SMTP
deactivate EmailDispatcher
deactivate Mediator
alt Email sent successfully
RegHandler->>RegHandler: emailSent = true
else mediator.Send throws
RegHandler->>RegHandler: catch(Exception), log to stderr, emailSent = false
Note right of Mediator: UserRegisteredNotificationHandler completes normally
else EmailDispatcher throws
Note right of Mediator: UserRegisteredNotificationHandler catches and logs the exception — registration is unaffected
end
deactivate Mediator
RegHandler-->>Mediator: RegistrationPayload(userAccountId, username, refreshToken, accessToken, confirmationEmailSent)
RegHandler-->>Mediator: RegistrationPayload(userAccountId, username, refreshToken, accessToken)
deactivate RegHandler
Mediator-->>API: RegistrationPayload
deactivate Mediator

View File

@@ -51,13 +51,20 @@ sequenceDiagram
TokenSvc-->>ResendHandler: confirmation token
deactivate TokenSvc
ResendHandler->>Mediator: Send(SendResendConfirmationEmailCommand)
ResendHandler->>Mediator: Publish(ConfirmationEmailResendRequestedNotification)
activate Mediator
Note right of Mediator: Features.Emails has a direct ProjectReference on Features.Users and handles the notification via ConfirmationEmailResendRequestedNotificationHandler
Mediator->>EmailDispatcher: SendResendConfirmationEmailAsync(firstName, email, confirmationToken)
activate EmailDispatcher
EmailDispatcher->>Template: Render email (same Razor pipeline as registration)
EmailDispatcher->>SMTP: SendAsync(email, subject, body)
deactivate EmailDispatcher
alt Email sent successfully
Note right of Mediator: ConfirmationEmailResendRequestedNotificationHandler completes normally
else EmailDispatcher throws
Note right of Mediator: ConfirmationEmailResendRequestedNotificationHandler catches and logs the exception — the resend request is unaffected
end
deactivate Mediator
end
end

View File

@@ -16,13 +16,14 @@ doesn't wire up any slice; it applies schema directly.
## Project reference dependencies
Solid arrows are compile-time project references. Dashed arrows are the two
places slices talk to each other without a direct reference: the email MediatR
contract routed through `Shared.Application`, and the one place that pattern is
broken outright. Every DB-backed slice depends on the same three foundation
projects (`Shared.Contracts`, `Domain.Entities`, `Database.Connection`) — drawn
as one box instead of fifteen separate arrows; see the notes for what else each
of those foundation projects pulls in.
Solid arrows are compile-time project references — every slice-to-slice
dependency is one now that the email MediatR contract has moved off
`Shared.Application`: `Features.Users` on `Features.PhotoUpload` for avatar
uploads, and `Features.Emails` on `Features.Users` for the registration and
resend-confirmation notifications. Every DB-backed slice depends on the same
three foundation projects (`Shared.Contracts`, `Domain.Entities`,
`Database.Connection`) — drawn as one box instead of fifteen separate arrows;
see the notes for what else each of those foundation projects pulls in.
```mermaid
--8<-- "web/diagrams/vertical-slice/project-references.mmd"
@@ -54,17 +55,20 @@ of those foundation projects pulls in.
- `Features.Beers` is scaffolded (project + references only, no source files
yet)
- **The one exception to slice isolation**: `Features.Users` takes a direct
project reference to `Features.PhotoUpload`
- sends its `UploadPhotoCommand` via MediatR from `UploadAvatarHandler`
- unlike the email flow, the command type lives in the target slice itself,
not in `Shared.Application`
- **The exceptions to slice isolation**:
- `Features.Users` takes a direct project reference to `Features.PhotoUpload`
- sends its `UploadPhotoCommand` via MediatR from `UploadAvatarHandler`
- the command type lives in the target slice itself, not in
`Shared.Application`
- `Features.Emails` takes a direct project reference to `Features.Users`
- handles `UserRegisteredNotification` (published from
`RegisterUserHandler`) and `ConfirmationEmailResendRequestedNotification`
(published from `ResendConfirmationEmailHandler`)
- both notification types live in `Features.Users`
(`Features.Auth.Notifications`), not in `Shared.Application`
- **Shared.Application**:
- `ValidationBehavior` (MediatR pipeline)
- Cross-slice email commands (`SendRegistrationEmailCommand`,
`SendResendConfirmationEmailCommand`)
- MediatR command contract (not project reference)
- Every slice (including `Features.Emails`) references it for the pipeline
behavior alone

View File

@@ -31,8 +31,7 @@ EmailTemplates["Infrastructure.Email.Templates"]
end
UsersSlice == project ref, sends UploadPhotoCommand ==> PhotoSlice
UsersSlice -. sends email commands .-> SharedApp["Shared.Application<br>(email command contract)"]
EmailsSlice -. handles email commands .-> SharedApp
EmailsSlice == project ref, handles UserRegisteredNotification & ConfirmationEmailResendRequestedNotification ==> UsersSlice
BrewerySlice --> FOUNDATION
LocationsSlice --> FOUNDATION
BeersSlice --> FOUNDATION