5. Framework Packages
The Notification Framework ships as 6 versioned NuGet packages from the Shumoul.Saas.NotificationFramework
repository, plus one small package (Shumoul.Notification.SignalR) consumed by the legacy hub host. This
chapter documents each package's purpose, responsibilities, public API surface, and forbidden responsibilities.
5.1 Shumoul.Notification.Contracts
Purpose: the shared vocabulary every other package and the ERP host speak. Zero ERP references, zero EF Core references.
Responsibilities:
- Enums:
NotificationChannel,DevicePlatform,SaaSNotificationType,NotificationDeliveryStatus,NotificationDeliveryState,NotificationFailureType,RetryStrategy,NotificationDeadLetterStatus,SaaSNotificationScheduleStatus,NotificationReceiptStatus(namespace...Contracts.Receipts),ProviderHealthState, campaign enums (CampaignType,CampaignStatus,CampaignExecutionStatus,RecurrenceType,DelayUnit,WorkflowStepTrigger,AudienceSourceType). - Cross-cutting DTOs:
DispatchNotificationRequest,DirectCommunicationRequest,ChannelDeliveryResult,ChannelDispatchContext,NotificationCountersDto,NotificationHistoryFilterDto,TenantNotificationLogDto/TenantNotificationLogDetailsDto,UserNotificationDto. - Settings POCOs:
SaaSNotificationSettings,PushSettings. - Constants:
NotificationSignalRConstants(hub contract),NotificationEventKeys(well-known event key strings),NotificationPermissions(permission string catalog). ErpCompat/sub-tree — a deliberate compatibility layer.ErpCompat/Enums/*.csandErpCompat/DTOs/*.csdeclare the same enum/DTO shapes but undernamespace Shumoul.Domain.Enums.Notifications/namespace Shumoul.Application.DTOs.Notificationsrespectively, so existing ERPusingstatements keep working unchanged. Some ErpCompat types are renamed from their native counterpart — e.g. nativeNotificationDeliveryStatusisNotificationStatusin ErpCompat. Treat ErpCompat as "what the ERP actually imports" and nativeContracts.*as "the cross-cutting SDK surface."
Public API: everything above is public; there is no internal-only surface in this package.
Forbidden responsibilities: must never reference Shumoul.Framework.Application, Shumoul.Domain, or
any EF Core package — this was enforced as part of Level 3 Golden Reference certification (Gap 1, closed:
the Shumoul.Framework.Application package reference was removed entirely from this project).
5.2 Shumoul.Notification.Abstractions
Purpose: the ports the ERP host must implement. No implementation logic lives here.
Responsibilities — one interface per aggregate root, deliberately not a "God Repository":
Repositories/:INotificationRepository,ITemplateRepository(+TemplateData),IDeviceTokenRepository(+DeviceTokenData),ICampaignRepository(+CampaignStateData),IDeliveryPolicyRepository(+DeliveryPolicyData),IDeliveryAttemptRepository(+NewDeliveryAttemptData),IRetryRepository(+DueRetryData,NewRetryData),IDeadLetterRepository(+NewDeadLetterData).Persistence/:INotificationPersistenceContext—SaveChangesAsync,BeginTransactionAsync,CommitAsync,RollbackAsync. The only interface in this package that owns transaction control.Core/(zero ERP references):IUserDirectory,ICurrentUserContext,ITenantContext,INotificationClock,INotificationConfigurationProvider.Adapters/(contracts the ERP host must implement):IUserDirectoryAdapter,INotificationRepositoryAdapter(aggregates all 8 repository interfaces as a unit-of-work view),ICurrentUserAdapter,ITenantContextAdapter.Providers/:INotificationChannelProvider(one implementation perNotificationChannel) andINotificationChannelProviderResolver.- Retry:
INotificationRetryStrategy,IRetryStrategyProvider.
Public API: all interfaces above.
Forbidden responsibilities: must never contain a concrete class, must never reference EF Core or
IRepositoryAsync directly, must never reference Shumoul.Framework.Application (Gap 1 closure, same as
Contracts).
5.3 Shumoul.Notification.Core
Purpose: the actual dispatch engine, retry strategies, and provider resolution/failover logic.
Responsibilities:
Services/Dispatch/NotificationDispatchService.cs— the real dispatch engine (see Chapter 6 — Runtime Flow). Resolves event configuration, selects templates per channel/language, resolves anINotificationChannelProviderper channel viaINotificationChannelProviderResolver, and hands off to it.Providers/: per-channel implementations ofINotificationChannelProvider—EmailChannelProvider,SmsChannelProvider,PushChannelProvider,SignalRChannelProvider,InAppChannelProvider. (WhatsApp has no dedicated channel-provider class — it is handled inline by the ERP-ownedWhatsAppNotificationSender, invoked directly by the dispatch path rather than through this resolver.)RetryStrategies/:ImmediateRetryStrategy,FixedRetryStrategy,LinearRetryStrategy,ExponentialBackoffRetryStrategy,NoRetryStrategy, resolved viaRetryStrategyProvider.Notifications/: channel service implementations —FirebasePushNotificationService(multi-provider failover viaINotificationProviderResolver/IProviderHealthMonitor),ScribanTemplateRenderer,EmailNotificationService,SmsNotificationService.- Provider health tracking:
InMemoryProviderHealthMonitor— a 5-consecutive-failure circuit breaker with 30-minute auto-recovery, keyed by provider name + channel.
Public API: NotificationDispatchService, all channel providers, all retry strategies, RetryStrategyProvider,
ScribanTemplateRenderer, EmailNotificationService, SmsNotificationService, FirebasePushNotificationService.
Forbidden responsibilities: must never contain ERP business rules (e.g. which purchase-invoice statuses
trigger a notification) — that decision belongs to the ERP host, expressed only as calls into
DispatchNotificationRequest.
Note on layered provider abstraction: Core actually has two composing abstraction layers, not one:
- Per-channel failover (
INotificationProviderResolver/IProviderHealthMonitor) — priority-ordered, health-tracked provider implementations per channel (e.g.ZohoEmailProvider,FourJawalySmsProvider,FirebasePushProvider,MetaWhatsAppProvider), consumed internally byEmailNotificationService,SmsNotificationService,FirebasePushNotificationService. - Unified channel dispatch (
INotificationChannelProvider/INotificationChannelProviderResolver) — sits above layer 1.EmailChannelProvider, for example, wrapsIEmailNotificationServicerather than replacing it.
Both layers are intentional, permanent architecture (confirmed by ADR-002's Gap 3 closure note), not a migration-in-progress.
5.4 Shumoul.Notification.Persistence
Purpose: entity classes and EF Core configuration for every framework-owned table.
Responsibilities:
NotificationEntity—public abstract class NotificationEntity : BaseEntity {}, an empty module-owned marker base class. Every framework entity inherits this instead ofBaseEntitydirectly, to keepShumoul.Framework.Domain.BaseEntityan implementation detail behind a module-owned seam (ADR-002, Gap 2 — accepted deviation:Shumoul.Framework.Domainremains a dependency of this project becauseBaseEntity/ICreationEntity/IAuditableEntity/ISoftDeleteare needed).- Entities (all declare
namespace Shumoul.Domain.Entities.Notificationsfor ERP compatibility):AppNotification,AppNotificationTemplate,DeviceToken,NotificationEventConfiguration,NotificationDeliveryPolicy,NotificationDeliveryAttempt,NotificationRetryQueue,NotificationDeadLetter,NotificationDeliveryReceipt, and 5 campaign entities (NotificationCampaign,NotificationCampaignAudience,NotificationCampaignExecution,NotificationCampaignStep,NotificationCampaignStepExecution). NotificationModelBuilderContributor— applies EF configuration forAppNotification,AppNotificationTemplate,DeviceToken,NotificationDeadLetter,NotificationDeliveryAttempt,NotificationDeliveryPolicy,NotificationEventConfiguration,NotificationRetryQueue.
Known gap (documented, not hidden): this contributor does not configure NotificationCampaign* or
NotificationDeliveryReceipt — those 6 tables are instead registered as DbSet<T> properties directly on
the ERP's own ApplicationDbContext, and were created by an ERP-side migration
(20260625113609_Add_NotificationDeliveryReceipt_Initial, whose name undersells its own scope — it actually
creates all 6 campaign/receipt tables in one migration).
Public API: the entity classes themselves, NotificationEntity, NotificationModelBuilderContributor.
Forbidden responsibilities: must never reference IRepositoryAsync or the ERP's ApplicationDbContext
directly — entities are plain EF Core POCOs; the ERP host's ApplicationDbContext and repository layer are
what actually query them.
5.5 Shumoul.Notification.SignalR
Purpose: the small, reusable slice of real-time infrastructure that both a framework-native SignalR
integration and the legacy NotificationHub in MultiTenancyApi can share.
Responsibilities:
NameIdentifierUserIdProvider : IUserIdProvider— resolves the SignalR-connected user's ID from the custom"NameIdentifier"JWT claim first, falling back to the standardClaimTypes.NameIdentifier. This is what makesClients.User(userId)resolve correctly for Shumoul's JWT shape.- Exposed via
AddShumoulNotificationSignalR()— a DI extension method that registers the provider.
Public API: NameIdentifierUserIdProvider, AddShumoulNotificationSignalR().
Forbidden responsibilities: must never contain a Hub class itself, must never reference
MultiTenancyApi-specific services (INotificationTopicSubscriberService, INotificationConnectionService)
— see §4.4.1 for why the
hub stays host-owned.
NotificationSignalRConstants (the canonical ClientMethod = "messages", TenantGroupPrefix = "GroupTenant-",
TopicGroupPrefix = "Topic-" values) lives in Shumoul.Notification.Contracts, not in this package, since
both the hub host and Angular/Flutter client documentation need to reference it without taking a SignalR
package dependency.
5.6 ERP Host Integration (Shumoul.Saas.Api adapters)
The ERP host does not consume the framework's Abstractions interfaces by reimplementing storage from
scratch — it adapts its existing IRepositoryAsync behind them:
| Adapter (ERP-owned) | Bridges |
|---|---|
DeliveryPolicyRepositoryAdapter (Shumoul.Infrastructure\Adapters\Notification\) | IRepositoryAsync → IDeliveryPolicyRepository/IDeliveryPolicyRepositoryAdapter |
NotificationRetryProcessorJobAdapter, NotificationRetryExpireJobAdapter, NotificationDeadLetterCleanupJobAdapter (Shumoul.Infrastructure\Adapters\BackgroundJobs\) | Background Jobs Framework IBackgroundJob → the framework's own retry/cleanup workers |
WhatsAppNotificationSender (Shumoul.Infrastructure\Services\Notifications\) | ERP-specific WhatsApp business templates → IWhatsAppService (from Shumoul.Saas.WhatsAppIntegration) |
This mirrors the same two-tier adapter pattern used by the platform's Background Jobs Framework: generic runtime in a package, ERP-specific wiring in a small adapter class inside the host.