In Production, users who start registration never receive their email-verification code. The pre-registration record is created and the API returns Success = true / EmailSent = true, but no email is actually sent — so the user is stuck (they can't verify their email, and SMS verification is gated behind email verification).
Evidence (SEQ)
A Warning is logged on every registration attempt:
Email send invoked on the API edge stub (method=SendEmailAsync, to=chhorton.80@gmail.com, subject=Verify Your Email - SpikerSoft Registration). Skipping outbound SMTP. Refactor the caller to publish SendEmailCommand : IRemoteCommand to SpikerSoft.EventHandlers.Notifications instead of resolving IEmailService directly.
During the API-edge slimming work, IEmailService on the API is bound to SpikerSoft.Business.Edge.Services.NoOpEmailService — a stub that logs a warning and drops the send (keeps MailKit out of the API publish closure). The real MailKit-backed EmailService runs only inside the SpikerSoft.EventHandlers.Notifications worker host.
InitiatePreRegistrationCommandHandler (in SpikerSoft.Business) still resolves IEmailService directly and calls SendEmailAsync(...) from its SendEmailVerification(...) helper (used by both the initial send and the resend path). On the API that call no-ops, so the verification email is silently dropped. The handler's try/catch then reports EmailSent = true because the no-op "succeeds", masking the failure.
The NoOpEmailService XML doc spells out the intended fix: callers should publish SendEmailCommand : IRemoteCommand onto the notifications exchange so the Notifications worker (which has the real EmailService) sends it.
Impact
Severity: High. New-user self-registration via the pre-registration flow is effectively broken in Production — no verification email ⇒ no account.
Misleading EmailSent = true in the API response and Success/Serilog signal, so the failure is invisible to the client and to dashboards that key off EmailSent.
Add a SendEmailCommandHandler in SpikerSoft.Workers.Notifications that resolves the real IEmailService (MailKit EmailService) and sends the message.
Register the new route in NotificationsRpcConsumerHostedService (fire-and-forget / no reply expected).
Refactor InitiatePreRegistrationCommandHandler.SendEmailVerification(...) to publish SendEmailCommand via MediatR (which RemoteDispatchBehavior publishes to RabbitMQ) instead of resolving IEmailService directly. This fixes both the initial-send and resend-email paths.
Acceptance criteria
Starting pre-registration in Production results in a verification email actually being delivered (sent by the Notifications worker).
No more NoOpEmailService "Skipping outbound SMTP" warning on the InitiatePreRegistration path.
EmailSent reflects whether the send command was successfully dispatched.
Unit coverage for the new worker handler + the refactored caller; existing pre-registration tests still pass.
Out of scope / follow-ups
The same NoOpEmailService warning will still fire for the other in-process callers listed in its XML doc (RegisterUser, CreateOrganization, ResendPreRegistrationCodes, ParentalEmailService). Those should be migrated to SendEmailCommand in follow-up tickets; this ticket is scoped to the reported registration (pre-registration) flow.
## Summary
In **Production**, users who start registration never receive their email-verification code. The pre-registration record is created and the API returns `Success = true` / `EmailSent = true`, but **no email is actually sent** — so the user is stuck (they can't verify their email, and SMS verification is gated behind email verification).
## Evidence (SEQ)
A `Warning` is logged on every registration attempt:
> Email send invoked on the API edge stub (method=`SendEmailAsync`, to=`chhorton.80@gmail.com`, subject=`Verify Your Email - SpikerSoft Registration`). Skipping outbound SMTP. Refactor the caller to publish `SendEmailCommand : IRemoteCommand` to `SpikerSoft.EventHandlers.Notifications` instead of resolving `IEmailService` directly.
Key log properties:
| Field | Value |
|---|---|
| `Environment` | Production |
| `ServiceName` | SpikerSoft.Api |
| `SourceContext` | `SpikerSoft.Business.Edge.Services.NoOpEmailService` |
| `ActionName` | `SpikerSoft.Api.Domain.Authentication.AuthenticationController.InitiatePreRegistration` |
| `RequestType` | `InitiatePreRegistrationCommand` |
| `Path` | `/api/authentication/initiate-pre-registration` |
| `Method` | `SendEmailAsync` |
## Root cause
During the API-edge slimming work, `IEmailService` on the API is bound to `SpikerSoft.Business.Edge.Services.NoOpEmailService` — a stub that **logs a warning and drops the send** (keeps MailKit out of the API publish closure). The real MailKit-backed `EmailService` runs only inside the `SpikerSoft.EventHandlers.Notifications` worker host.
`InitiatePreRegistrationCommandHandler` (in `SpikerSoft.Business`) still resolves `IEmailService` directly and calls `SendEmailAsync(...)` from its `SendEmailVerification(...)` helper (used by both the initial send and the resend path). On the API that call no-ops, so the verification email is silently dropped. The handler's `try/catch` then reports `EmailSent = true` because the no-op "succeeds", masking the failure.
The `NoOpEmailService` XML doc spells out the intended fix: callers should **publish `SendEmailCommand : IRemoteCommand`** onto the `notifications` exchange so the Notifications worker (which has the real `EmailService`) sends it.
## Impact
- **Severity: High.** New-user self-registration via the pre-registration flow is effectively broken in Production — no verification email ⇒ no account.
- Misleading `EmailSent = true` in the API response and `Success`/Serilog signal, so the failure is invisible to the client and to dashboards that key off `EmailSent`.
## Proposed fix
1. Add a generic `SendEmailCommand : IRemoteCommand` to `SpikerSoft.Contracts.Workers` (exchange `notifications`, routing key `notifications.email.send.requested`) carrying `ToEmail` / `Subject` / `BodyHtml` / `BodyText`.
2. Add a `SendEmailCommandHandler` in `SpikerSoft.Workers.Notifications` that resolves the real `IEmailService` (MailKit `EmailService`) and sends the message.
3. Register the new route in `NotificationsRpcConsumerHostedService` (fire-and-forget / no reply expected).
4. Refactor `InitiatePreRegistrationCommandHandler.SendEmailVerification(...)` to publish `SendEmailCommand` via MediatR (which `RemoteDispatchBehavior` publishes to RabbitMQ) instead of resolving `IEmailService` directly. This fixes both the initial-send and resend-email paths.
## Acceptance criteria
- [ ] Starting pre-registration in Production results in a verification email actually being delivered (sent by the Notifications worker).
- [ ] No more `NoOpEmailService` "Skipping outbound SMTP" warning on the `InitiatePreRegistration` path.
- [ ] `EmailSent` reflects whether the send command was successfully dispatched.
- [ ] Unit coverage for the new worker handler + the refactored caller; existing pre-registration tests still pass.
## Out of scope / follow-ups
The same `NoOpEmailService` warning will still fire for the other in-process callers listed in its XML doc (`RegisterUser`, `CreateOrganization`, `ResendPreRegistrationCodes`, `ParentalEmailService`). Those should be migrated to `SendEmailCommand` in follow-up tickets; this ticket is scoped to the reported registration (pre-registration) flow.
Resolved in spikersoft-backend PR #8 (merged to master at 2026-06-15T23:14:29Z).
InitiatePreRegistrationCommandHandler no longer resolves IEmailService directly (the API-edge NoOpEmailService silently dropped the SMTP send, so verification emails never went out while the handler still reported EmailSent = true). It now publishes SendEmailCommand : IRemoteCommand to the notifications exchange; the new SendEmailCommandHandler in SpikerSoft.Workers.Notifications performs the real MailKit send, and NotificationsRpcConsumerHostedService gained fire-and-forget routing for it. Covers both the initial-send and resend-email paths.
Closing.
Resolved in spikersoft-backend PR #8 (merged to `master` at 2026-06-15T23:14:29Z).
`InitiatePreRegistrationCommandHandler` no longer resolves `IEmailService` directly (the API-edge `NoOpEmailService` silently dropped the SMTP send, so verification emails never went out while the handler still reported `EmailSent = true`). It now publishes `SendEmailCommand : IRemoteCommand` to the `notifications` exchange; the new `SendEmailCommandHandler` in `SpikerSoft.Workers.Notifications` performs the real MailKit send, and `NotificationsRpcConsumerHostedService` gained fire-and-forget routing for it. Covers both the initial-send and resend-email paths.
Closing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
In Production, users who start registration never receive their email-verification code. The pre-registration record is created and the API returns
Success = true/EmailSent = true, but no email is actually sent — so the user is stuck (they can't verify their email, and SMS verification is gated behind email verification).Evidence (SEQ)
A
Warningis logged on every registration attempt:Key log properties:
EnvironmentServiceNameSourceContextSpikerSoft.Business.Edge.Services.NoOpEmailServiceActionNameSpikerSoft.Api.Domain.Authentication.AuthenticationController.InitiatePreRegistrationRequestTypeInitiatePreRegistrationCommandPath/api/authentication/initiate-pre-registrationMethodSendEmailAsyncRoot cause
During the API-edge slimming work,
IEmailServiceon the API is bound toSpikerSoft.Business.Edge.Services.NoOpEmailService— a stub that logs a warning and drops the send (keeps MailKit out of the API publish closure). The real MailKit-backedEmailServiceruns only inside theSpikerSoft.EventHandlers.Notificationsworker host.InitiatePreRegistrationCommandHandler(inSpikerSoft.Business) still resolvesIEmailServicedirectly and callsSendEmailAsync(...)from itsSendEmailVerification(...)helper (used by both the initial send and the resend path). On the API that call no-ops, so the verification email is silently dropped. The handler'stry/catchthen reportsEmailSent = truebecause the no-op "succeeds", masking the failure.The
NoOpEmailServiceXML doc spells out the intended fix: callers should publishSendEmailCommand : IRemoteCommandonto thenotificationsexchange so the Notifications worker (which has the realEmailService) sends it.Impact
EmailSent = truein the API response andSuccess/Serilog signal, so the failure is invisible to the client and to dashboards that key offEmailSent.Proposed fix
SendEmailCommand : IRemoteCommandtoSpikerSoft.Contracts.Workers(exchangenotifications, routing keynotifications.email.send.requested) carryingToEmail/Subject/BodyHtml/BodyText.SendEmailCommandHandlerinSpikerSoft.Workers.Notificationsthat resolves the realIEmailService(MailKitEmailService) and sends the message.NotificationsRpcConsumerHostedService(fire-and-forget / no reply expected).InitiatePreRegistrationCommandHandler.SendEmailVerification(...)to publishSendEmailCommandvia MediatR (whichRemoteDispatchBehaviorpublishes to RabbitMQ) instead of resolvingIEmailServicedirectly. This fixes both the initial-send and resend-email paths.Acceptance criteria
NoOpEmailService"Skipping outbound SMTP" warning on theInitiatePreRegistrationpath.EmailSentreflects whether the send command was successfully dispatched.Out of scope / follow-ups
The same
NoOpEmailServicewarning will still fire for the other in-process callers listed in its XML doc (RegisterUser,CreateOrganization,ResendPreRegistrationCodes,ParentalEmailService). Those should be migrated toSendEmailCommandin follow-up tickets; this ticket is scoped to the reported registration (pre-registration) flow.Resolved in spikersoft-backend PR #8 (merged to
masterat 2026-06-15T23:14:29Z).InitiatePreRegistrationCommandHandlerno longer resolvesIEmailServicedirectly (the API-edgeNoOpEmailServicesilently dropped the SMTP send, so verification emails never went out while the handler still reportedEmailSent = true). It now publishesSendEmailCommand : IRemoteCommandto thenotificationsexchange; the newSendEmailCommandHandlerinSpikerSoft.Workers.Notificationsperforms the real MailKit send, andNotificationsRpcConsumerHostedServicegained fire-and-forget routing for it. Covers both the initial-send and resend-email paths.Closing.