Architecture and Core Concepts
This guide explains why Pragmatic.Abstractions exists, how its contracts shape the entire ecosystem, and how to reason about what belongs in this package versus in a specific module.
The Problem
Section titled “The Problem”A modular framework needs shared contracts. Without a central place for those contracts, two bad things happen.
Circular dependencies
Section titled “Circular dependencies”Persistence needs ICurrentUser for auditing (CreatedBy, UpdatedBy). Identity needs IRepository for user storage. If each module owns its own interfaces, they depend on each other:
Pragmatic.Persistence ─── depends on ──→ Pragmatic.Identity (for ICurrentUser) ↑ │ └──────── depends on ────────────────────┘ (for IRepository)This is a circular dependency. The compiler rejects it. Developers work around it with hacks: reflection, object parameters, separate interface packages per module. Each workaround adds complexity.
Framework coupling
Section titled “Framework coupling”ASP.NET Core provides ClaimsPrincipal, HttpContext, IAuthorizationService. EF Core provides DbContext, IQueryable<T>. If domain modules reference these directly:
// Every service that needs the current user now depends on ASP.NET Corepublic class AuditingInterceptor(IHttpContextAccessor accessor){ public void BeforeSave(IAuditable entity) { var user = accessor.HttpContext?.User; entity.CreatedBy = user?.FindFirst("sub")?.Value; // Tied to claims structure }}- Console apps, background workers, and test harnesses cannot use these services without pulling in the entire ASP.NET Core stack.
- Module portability is lost. A module written against
HttpContextcannot run in a gRPC host, a message handler, or a Lambda function. - Testing requires mocking framework types (
ClaimsPrincipal,HttpContext) instead of simple interfaces.
Interface duplication
Section titled “Interface duplication”Without a shared package, every module that needs “the current user” would define its own interface:
// In Pragmatic.Persistencepublic interface IAuditUser { string? Id { get; } }
// In Pragmatic.Actionspublic interface IActionUser { string Id { get; } bool HasPermission(string p); }
// In Pragmatic.Eventspublic interface IEventUser { string Id { get; } }Three interfaces for the same concept. They cannot be composed. A single DI registration cannot satisfy all three. Every module re-implements the mapping from ClaimsPrincipal to its own interface.
The Solution
Section titled “The Solution”Pragmatic.Abstractions is a single, lightweight package that contains only the contracts (interfaces, attributes, records, enums) consumed across module boundaries. It has zero dependency on ASP.NET Core or EF Core.
Pragmatic.Abstractions (Layer 0) ^ | +----------------+------------------+ | | | Pragmatic.Actions Pragmatic.Events Pragmatic.Persistence.EFCore Pragmatic.Identity Pragmatic.Authorization ... (Layer 1-2 modules)Every module depends on Abstractions. Abstractions depends on nothing except three minimal packages:
| Dependency | Purpose |
|---|---|
Pragmatic.Specification | Specification<T> base class used by IReadRepository |
Microsoft.Extensions.Configuration.Abstractions | IConfiguration for IPragmaticBuilder |
Microsoft.Extensions.DependencyInjection.Abstractions | IServiceCollection for IPragmaticBuilder and Decorate extensions |
Microsoft.Extensions.Hosting.Abstractions | IHostEnvironment for IPragmaticBuilder |
These three Microsoft packages are stable, widely-used .NET abstractions with no runtime coupling. They are accepted as foundational because IPragmaticBuilder needs IServiceCollection, IConfiguration, and IHostEnvironment to function.
What this enables
Section titled “What this enables”- No circular dependencies.
PersistenceandIdentityboth reference Abstractions, not each other. - Framework independence. Domain modules depend on
ICurrentUser, notClaimsPrincipal. The ASP.NET Core adapter lives inPragmatic.Identity.AspNetCoreand is only referenced by the host. - Single interface per concept. One
ICurrentUser, oneIClock, oneIRepository. Every module consumes the same contract. One DI registration satisfies all consumers. - Lightweight referencing. A console app, test harness, or background worker can reference Abstractions without pulling ASP.NET Core or EF Core.
Interface Catalog
Section titled “Interface Catalog”The catalog below organizes every public type by domain. For full member-level signatures, see interfaces.md.
Result
Section titled “Result”| Type | Kind | Namespace |
|---|---|---|
IError | Interface | Pragmatic.Result |
Base contract for all error types. Declares Code (UPPER_SNAKE_CASE semantic identifier), StatusCode (HTTP status code), Title (Problem Details title), and Description. Used by the Result pattern (Result<T, TError>) for type-safe error handling without exceptions.
Consumers: Result, Actions, Endpoints, Source Generator.
Persistence — Entities
Section titled “Persistence — Entities”| Type | Kind | Namespace |
|---|---|---|
IEntity | Interface | Pragmatic.Persistence.Entity |
IEntity<TId> | Interface | Pragmatic.Persistence.Entity |
IAuditable | Interface | Pragmatic.Persistence.Entity |
ISoftDelete | Interface | Pragmatic.Persistence.Entity |
IChangeTracking | Interface | Pragmatic.Persistence.Entity |
ITemporalRelation | Interface | Pragmatic.Persistence.Entity |
IEntityFactory<TEntity> | Interface | Pragmatic.Persistence.Entity |
IEntity is the marker interface for all entities. IEntity<TId> adds a typed PersistenceId property. IAuditable tracks creation and update timestamps plus who performed them. ISoftDelete enables logical deletion with IsDeleted, DeletedAt, and DeletedBy. IChangeTracking provides property-level change tracking (ModifiedProperties, IsNew) used for selective validation, audit trails, and optimized persistence. ITemporalRelation marks time-bounded relationships with ValidFrom and ValidTo. IEntityFactory<TEntity> is a factory consumed by MutationInvoker when entities need complex initialization.
The source generator auto-implements these interfaces on entities marked with [Entity<TId>]. The Persistence.EFCore interceptors populate IAuditable and ISoftDelete fields at runtime.
Persistence — Repositories
Section titled “Persistence — Repositories”| Type | Kind | Namespace |
|---|---|---|
IReadRepository<TEntity, TId> | Interface | Pragmatic.Persistence.Repository |
IRepository<TEntity, TId> | Interface | Pragmatic.Persistence.Repository |
IUnitOfWork | Interface | Pragmatic.Persistence.Repository |
ITransaction | Interface | Pragmatic.Persistence.Repository |
IReadRepository provides read-only queries: GetByIdAsync, FindAsync (using Specification<T>), CountAsync, ExistsAsync, FirstOrDefaultAsync, and Query() for raw IQueryable access. IRepository extends it with Add, AddRange, Remove, RemoveRange, and Update. IUnitOfWork manages transactions via SaveChangesAsync and BeginTransactionAsync. ITransaction represents a database transaction with CommitAsync and RollbackAsync.
Implemented by: Pragmatic.Persistence.EFCore. The source generator produces typed repository classes nested inside entity types.
Identity
Section titled “Identity”| Type | Kind | Namespace |
|---|---|---|
ICurrentUser | Interface | Pragmatic.Identity |
IAuthenticationContext | Interface | Pragmatic.Identity |
IUserProfile | Interface | Pragmatic.Identity |
PrincipalKind | Enum | Pragmatic.Identity |
AnonymousUser | Sealed class | Pragmatic.Identity |
NullAuthenticationContext | Sealed class | Pragmatic.Identity |
CurrentUserExtensions | Static class | Pragmatic.Identity |
[PragmaticUser] | Attribute | Pragmatic.Identity |
[ProfileProperty] | Attribute | Pragmatic.Identity |
ICurrentUser is the central identity contract. It carries Id, DisplayName, IsAuthenticated, Kind (Anonymous, User, Service, System), TenantId, Claims (multi-valued dictionary), and ImpersonatedBy. Authorization and authentication concerns are separated into sub-objects: IUserAuthorization for permission/role checks and IAuthenticationContext for authentication metadata (scheme, issuer, MFA status, expiration).
AnonymousUser is a singleton null-object where all checks return false. NullAuthenticationContext is a singleton where all properties return null/false. Both have private constructors and a public static readonly Instance field.
Consumers: Persistence (auditing), Actions (authorization filter), Endpoints (permission enforcement), Logging (correlation).
Authorization
Section titled “Authorization”| Type | Kind | Namespace |
|---|---|---|
IUserAuthorization | Interface | Pragmatic.Authorization |
IPermission | Interface | Pragmatic.Authorization |
IRole | Interface | Pragmatic.Authorization |
IGroup | Interface | Pragmatic.Authorization |
IRoleDefinition | Interface | Pragmatic.Authorization |
IPermissionProvider | Interface | Pragmatic.Authorization |
IPermissionChecker | Interface | Pragmatic.Authorization |
IResourceAuthorizer<TResource> | Interface | Pragmatic.Authorization |
PermissionInfo | Record | Pragmatic.Authorization |
RoleInfo | Record | Pragmatic.Authorization |
NullUserAuthorization | Sealed class | Pragmatic.Authorization |
FullAccessUserAuthorization | Sealed class | Pragmatic.Authorization |
[RequirePermission] | Attribute | Pragmatic.Authorization |
[RequireAnyPermission] | Attribute | Pragmatic.Authorization |
[ExplicitPermission] / [ExplicitPermission<T>] | Attribute | Pragmatic.Authorization |
IUserAuthorization provides the runtime authorization context: Roles, Permissions, Groups, Scopes, and methods like HasPermission, HasAnyPermission, HasAllPermissions, IsInRole, IsInGroup, HasScope.
IPermission, IRole, and IGroup use static abstract members (Name, Description, Category/DefaultPermissions/DefaultRoles) for compile-time safety. Each permission, role, and group is a type, not a string. This enables IntelliSense, refactoring support, and SG-generated registries.
IPermissionProvider resolves permissions for a user from external sources (JWT, database, policy server). IPermissionChecker is the async counterpart for I/O-bound checks. IResourceAuthorizer<TResource> (contravariant on TResource) enables resource-level (ABAC) authorization.
NullUserAuthorization returns false for all checks (used by AnonymousUser). FullAccessUserAuthorization returns true for all checks (used for system-level contexts).
Events
Section titled “Events”| Type | Kind | Namespace |
|---|---|---|
IDomainEvent | Interface | Pragmatic.Events |
IDomainEventDispatcher | Interface | Pragmatic.Events |
IDomainEventHandler<TEvent> | Interface | Pragmatic.Events |
IHasDomainEvents | Interface | Pragmatic.Events |
EntityPropertyChanged<TEntity> | Record | Pragmatic.Events |
IDomainEvent is the marker for all domain events, declaring OccurredAt. IDomainEventDispatcher dispatches events to handlers. IDomainEventHandler<TEvent> handles a specific event type with ordering support via Order. IHasDomainEvents marks entities that raise domain events. EntityPropertyChanged<TEntity> is a built-in event for property change cascades.
Temporal
Section titled “Temporal”| Type | Kind | Namespace |
|---|---|---|
IClock | Interface | Pragmatic.Temporal.Clock |
Testable time abstraction: UtcNow, Now, UtcToday, Today, UtcTimeOfDay, TimeOfDay, and GetTimeProvider() for .NET 8+ interop. Use IClock instead of DateTime.Now or DateTimeOffset.UtcNow for testability.
Implemented by: SystemClock (production) and TestClock (testing) in Pragmatic.Temporal.
Multi-Tenancy
Section titled “Multi-Tenancy”| Type | Kind | Namespace |
|---|---|---|
ITenantContext | Interface | Pragmatic.MultiTenancy |
ITenantResolver | Interface | Pragmatic.MultiTenancy |
ITenantEntity | Interface | Pragmatic.MultiTenancy |
UnresolvedTenantContext | Sealed class | Pragmatic.MultiTenancy |
ITenantContext provides the current tenant’s identity: TenantId, TenantName, IsResolved. ITenantResolver resolves tenant identity from ambient context (transport-agnostic). ITenantEntity is a marker for row-level tenant isolation with a TenantId property.
UnresolvedTenantContext is the null-object singleton when no tenant is resolved.
Consumers: Persistence (tenant query filter, connection string per tenant), Caching (tenant-scoped keys), Configuration (tenant overrides).
Caching
Section titled “Caching”| Type | Kind | Namespace |
|---|---|---|
ICacheStack | Interface | Pragmatic.Caching |
CacheEntryOptions | Class | Pragmatic.Caching |
CachePriority | Enum | Pragmatic.Caching |
CacheCategories | Static class | Pragmatic.Caching |
CacheCategories.Default | Class | Pragmatic.Caching |
CacheCategories.OutputCache | Class | Pragmatic.Caching |
CacheCategories.RateLimiting | Class | Pragmatic.Caching |
CacheCategories.Permissions | Class | Pragmatic.Caching |
CacheCategories.Configuration | Class | Pragmatic.Caching |
ICacheStack is the unified caching abstraction: GetOrSetAsync (stampede-protected), GetAsync, TryGetAsync, SetAsync, RemoveAsync, InvalidateByTagAsync, InvalidateByTagsAsync. CacheEntryOptions specifies Duration, SlidingDuration, Tags, and Priority. CacheCategories defines marker types for routing cache operations to different backends.
The caching types live in Abstractions so that modules like Authorization, Configuration, and Endpoints can depend on the caching abstraction without referencing Pragmatic.Caching.
Configuration
Section titled “Configuration”| Type | Kind | Namespace |
|---|---|---|
IConfigurationStore | Interface | Pragmatic.Configuration |
ISecretStore | Interface | Pragmatic.Configuration |
ConfigurationChange | Record | Pragmatic.Configuration |
EnvironmentProfile | Class | Pragmatic.Configuration |
IConfigurationStore is a backend-agnostic config store with tenant-scoped overrides: GetAsync, GetSectionAsync, SetAsync, DeleteAsync, WatchAsync. ISecretStore is a read-only secret store. EnvironmentProfile wraps IHostEnvironment with Pragmatic conventions.
Feature Flags
Section titled “Feature Flags”| Type | Kind | Namespace |
|---|---|---|
IFeatureFlag | Interface | Pragmatic.FeatureFlags |
IFeatureFlagStore | Interface | Pragmatic.FeatureFlags |
IFeatureFlagContextProvider | Interface | Pragmatic.FeatureFlags |
FeatureFlagContext | Record | Pragmatic.FeatureFlags |
FeatureFlagDefinition | Record | Pragmatic.FeatureFlags |
FeatureFlagRule | Record | Pragmatic.FeatureFlags |
FeatureFlagChange | Record | Pragmatic.FeatureFlags |
FeatureFlagStoreExtensions | Static class | Pragmatic.FeatureFlags |
IFeatureFlag uses static abstract members (Name, Description) so each flag is a type. This enables generic methods like store.IsEnabledAsync<LoyaltyDiscount>() with compile-time safety instead of magic strings.
Internationalization
Section titled “Internationalization”| Type | Kind | Namespace |
|---|---|---|
IGlobalizationContext | Interface | Pragmatic.Internationalization.Context |
Provides the current globalization context: Culture, TimeZone, and CurrencyCode. TimeZone and CurrencyCode have default implementations returning null (falls back to UTC and culture-derived currency respectively).
Pipeline
Section titled “Pipeline”| Type | Kind | Namespace |
|---|---|---|
ICallContext | Interface | Pragmatic.Pipeline |
Tracks whether the current execution is an internal (system-initiated) call: IsInternalCall and EnterInternalCall() (returns a disposable that restores previous state, supports nesting). When IsInternalCall is true, authorization filters skip permission checks. Event handlers automatically run as internal calls.
This interface exists in Abstractions to break the dependency between Actions (which implements it as ActionCallContext) and Events (which checks IsInternalCall when dispatching events).
Composition
Section titled “Composition”| Type | Kind | Namespace |
|---|---|---|
IPragmaticBuilder | Interface | Pragmatic.Composition |
IPackageDefinition | Interface | Pragmatic.Composition |
IPragmaticBuilder is the fluent builder for module strategy configuration at startup. It exposes Services (IServiceCollection), Configuration (IConfiguration), and Environment (IHostEnvironment). Each module contributes Use*() extension methods on this interface (e.g., UseMultiTenancy, UseAuthentication). The interface lives in Abstractions so every module can extend it without referencing the host.
IPackageDefinition defines a package that integrates into any host via [UsePackage<T>]. It uses static abstract members: PackageName, RoutePrefix, Description, and RequiredTypeNames.
Implemented by: PragmaticBuilder in Pragmatic.Composition.Host.
Composition Attributes
Section titled “Composition Attributes”| Attribute | Target | Namespace |
|---|---|---|
[Module] | Class | Pragmatic.Composition.Attributes |
[Service] / [Service<TInterface>] | Class | Pragmatic.Composition.Attributes |
[Decorator] | Class | Pragmatic.Composition.Attributes |
[Inject] | Property/Method | Pragmatic.Composition.Attributes |
[ServiceFactory] / [Factory] | Class/Method | Pragmatic.Composition.Attributes |
[Include<TModule>] | Class | Pragmatic.Composition.Attributes |
[Include<TModule, TDatabase>] | Class | Pragmatic.Composition.Attributes |
[Include<TModule, TDatabase, TDbContext>] | Class | Pragmatic.Composition.Attributes |
[IncludeModule<TModule>] | Class | Pragmatic.Composition.Attributes |
[DependsOn<TModule>] | Class (deprecated) | Pragmatic.Composition.Attributes |
[StartupStep] | Class | Pragmatic.Composition.Attributes |
[PragmaticDatabase] | Class | Pragmatic.Composition.Attributes |
[RequiresConfig] | Class | Pragmatic.Composition.Attributes |
[UsePackage<TPackage>] | Class | Pragmatic.Composition.Attributes |
[RemoteBoundary<TModule>] | Class | Pragmatic.Composition.Attributes |
[PragmaticMetadata] | Assembly | Pragmatic.Composition.Attributes |
These attributes are consumed by the source generator at compile time. The SG reads them to generate DI registrations, topology reports, module dependency graphs, and host wiring code.
Key patterns:
[Service]registers a class for DI. Default: scoped, registered as its first interface. Use[Service<TInterface>]for explicit interface registration.[Include<TModule>]/[Include<TModule, TDatabase>]are host-level declarations that wire modules to databases.[IncludeModule<TModule>]is a library-level declaration of module dependency.[UsePackage<TPackage>]imports a package’s metadata into the module.[RemoteBoundary<TModule>]declares a module as remote (HTTP invocation instead of in-process).
Composition Enums and Base Classes
Section titled “Composition Enums and Base Classes”| Type | Namespace | Description |
|---|---|---|
ServiceLifetime | Pragmatic.Composition.Attributes | Singleton, Scoped, Transient |
DatabaseProvider | Pragmatic.Composition.Enums | SqlServer, PostgreSql, SQLite, MySql, InMemory |
MetadataCategory | Pragmatic.Composition.Metadata | 15 categories for SG-generated assembly metadata |
PragmaticDatabase | Pragmatic.Composition.Database | Abstract base class for database declarations |
Composition Extensions
Section titled “Composition Extensions”| Type | Namespace |
|---|---|
ServiceCollectionDecorateExtensions | Pragmatic.Composition.Extensions |
Extension methods for IServiceCollection.Decorate<TService, TDecorator>() and factory-based decoration. This lives in Abstractions so domain modules can use decoration without referencing ASP.NET Core or Pragmatic.Composition.Host.
Telemetry
Section titled “Telemetry”| Type | Kind | Namespace |
|---|---|---|
ActivityHelper | Static class | Pragmatic.Telemetry |
TelemetryOptions | Class | Pragmatic.Telemetry |
ActivityHelper provides extension methods for System.Diagnostics.Activity: RecordException, SetSuccess, SetFailure, AddNamedEvent. TelemetryOptions configures OpenTelemetry: Enabled, Tracing, Metrics, Logging, UseOtlpExporter, ServiceName, SamplingRatio.
Telemetry Conventions
Section titled “Telemetry Conventions”Standardized tag name constants for OpenTelemetry instrumentation across all modules.
| Class | Prefix | Namespace |
|---|---|---|
ErrorTags | error.*, exception.* | Pragmatic.Telemetry.Conventions |
ActionTags | pragmatic.action.*, pragmatic.mutation.* | Pragmatic.Telemetry.Conventions |
DbTags | db.*, pragmatic.db.* | Pragmatic.Telemetry.Conventions |
CacheTags | cache.*, pragmatic.cache.* | Pragmatic.Telemetry.Conventions |
ResilienceTags | pragmatic.resilience.* | Pragmatic.Telemetry.Conventions |
EventTags | pragmatic.event.* | Pragmatic.Telemetry.Conventions |
I18NTags | pragmatic.i18n.*, pragmatic.localization.* | Pragmatic.Telemetry.Conventions |
Tag constants are centralized here to prevent tag name drift across modules. Standard OTel prefixes (db.*, exception.*, cache.*) follow OpenTelemetry semantic conventions. pragmatic.* prefixes are framework-specific extensions for concepts with no OTel equivalent.
Layer Architecture
Section titled “Layer Architecture”Pragmatic.Design organizes packages into three layers based on their dependency direction.
Layer 0 (Foundation) Layer 1 (Capabilities) Layer 2 (Integration)├── Result ├── Validation ├── Actions├── Ensure ├── Mapping ├── Endpoints├── DependencyInjection ├── Internationalization ├── Persistence.EFCore├── Abstractions ◄──────────┤── Resilience ├── Composition.Host└── Specification ├── Configuration └── Identity.AspNetCore ├── Caching └── SpecificationLayer 0 has no upward dependencies. Every module in the ecosystem can reference Layer 0 packages.
Layer 1 modules reference Layer 0 and provide specific capabilities. They do not depend on each other unless explicitly declared.
Layer 2 modules integrate multiple capabilities and may reference Layer 1 packages. They provide the “glue” between the domain and the infrastructure (HTTP, database, hosting).
Abstractions sits at Layer 0. It is the foundation that all other layers build upon.
Design Principles
Section titled “Design Principles”1. No ASP.NET Core references
Section titled “1. No ASP.NET Core references”Abstractions must never reference Microsoft.AspNetCore.*. This ensures:
- Console apps and background workers can reference Abstractions without the web stack.
- Domain modules remain portable across hosting models (Kestrel, IIS, Lambda, gRPC).
- Test projects can mock interfaces without importing ASP.NET Core test infrastructure.
The three Microsoft.Extensions.*.Abstractions packages are explicitly allowed because they are part of the .NET platform (not ASP.NET) and are required by IPragmaticBuilder.
2. No EF Core references
Section titled “2. No EF Core references”Abstractions must never reference Microsoft.EntityFrameworkCore. Repository interfaces (IRepository<T, TId>) are defined here; their EF Core implementations live in Pragmatic.Persistence.EFCore.
3. Interfaces over implementations
Section titled “3. Interfaces over implementations”The package contains interfaces, attributes, records, and enums. Implementation logic is prohibited except for:
- Null-object singletons (
AnonymousUser,NullUserAuthorization,UnresolvedTenantContext, etc.) — trivial implementations that return empty/false/null for every member. - Extension methods (
CurrentUserExtensions,ServiceCollectionDecorateExtensions,FeatureFlagStoreExtensions) — stateless utility methods that compose existing interfaces. - Telemetry helpers (
ActivityHelper) — stateless extension methods onSystem.Diagnostics.Activity.
4. Stable contracts
Section titled “4. Stable contracts”Because every module depends on Abstractions, breaking changes cascade to the entire ecosystem. Types added here should represent stable concepts that change infrequently. Volatile or experimental types belong in the specific module until they stabilize.
When adding new members to existing interfaces, use default interface implementations to avoid breaking existing implementors:
public interface IError{ string Code { get; } int StatusCode { get; } string Title => string.Empty; // Default implementation -- non-breaking string? Description => null; // Default implementation -- non-breaking}5. Strongly typed over magic strings
Section titled “5. Strongly typed over magic strings”Abstractions favors static abstract interface members (C# 11+) for compile-time safety:
public interface IPermission{ static abstract string Name { get; } static abstract string? Description { get; } static abstract string? Category { get; }}Each permission, role, group, and feature flag is a type, not a string. This enables:
- Compile-time validation via the source generator.
- IntelliSense and refactoring support.
- Generic extension methods like
store.IsEnabledAsync<LoyaltyDiscount>(). - SG-generated registries and constants.
The generic attribute convention follows: [ExplicitPermission<TPermission>] rather than [ExplicitPermission(typeof(TPermission))].
6. Null-object pattern
Section titled “6. Null-object pattern”For interfaces consumed in contexts where no real implementation may exist (anonymous requests, no-tenant scenarios), Abstractions provides singleton null-objects:
| Null Object | Interface | Behavior |
|---|---|---|
AnonymousUser | ICurrentUser | Id = empty, IsAuthenticated = false, all authorization = false |
NullAuthenticationContext | IAuthenticationContext | All properties = null/false |
NullUserAuthorization | IUserAuthorization | All checks return false, all collections empty |
FullAccessUserAuthorization | IUserAuthorization | All checks return true (system-level contexts) |
UnresolvedTenantContext | ITenantContext | TenantId = null, IsResolved = false |
These are sealed classes with private constructors and a public static readonly Instance field. They are safe to register as singletons in DI. This prevents consumers from needing to handle null service resolution.
7. Attribute design guidelines
Section titled “7. Attribute design guidelines”Attributes in Abstractions follow these rules:
- Generic over
typeof: Always[Attr<T>]over[Attr(typeof(T))]. - Minimal properties: Only properties that affect SG output or runtime behavior.
- No implementation logic: Attributes are pure data carriers.
Inherited = false: Most attributes are not inherited (each type opts in explicitly).AllowMultipleonly when needed:[Include<T>]allows multiple (a host may include many modules). Most other attributes are single-use.
RootNamespace Convention
Section titled “RootNamespace Convention”The .csproj declares:
<RootNamespace>Pragmatic</RootNamespace>All types live under Pragmatic.* namespaces (e.g., Pragmatic.Identity.ICurrentUser, Pragmatic.Events.IDomainEvent), not under Pragmatic.Abstractions.*. This is intentional.
When a consumer writes using Pragmatic.Identity;, they get the same namespace regardless of whether they reference the Abstractions package or the full Identity package. The abstraction and its implementation share a namespace, which simplifies imports and means switching from the abstraction to the full module requires no using changes.
How Modules Reference Abstractions
Section titled “How Modules Reference Abstractions”Module runtime packages
Section titled “Module runtime packages”Module runtime packages (e.g., Pragmatic.Actions, Pragmatic.Persistence.EFCore) take a <ProjectReference> on Abstractions. This gives them access to all shared contracts:
<!-- In Pragmatic.Actions.csproj --><ProjectReference Include="..\..\Pragmatic.Abstractions\src\Pragmatic.Abstractions\Pragmatic.Abstractions.csproj" />Host projects
Section titled “Host projects”Host projects (the final executable) transitively receive Abstractions through module references. A host that references Pragmatic.Composition.Host and Pragmatic.Persistence.EFCore automatically has access to all Abstractions types.
Source generator
Section titled “Source generator”The source generator (Pragmatic.SourceGenerator) reads attribute definitions from Abstractions at compile time. It does not take a direct reference but resolves types via GetTypeByMetadataName in the Roslyn compilation model. The attribute types must be available in the compilation (which they are when the consuming project references Abstractions or any module that transitively references it).
Test projects
Section titled “Test projects”Test projects can reference Abstractions directly to mock interfaces without importing runtime modules:
// Test can mock ICurrentUser without referencing Pragmatic.Identity.AspNetCorevar mockUser = Substitute.For<ICurrentUser>();mockUser.Id.Returns("test-user-123");mockUser.IsAuthenticated.Returns(true);Breaking Circular Dependencies
Section titled “Breaking Circular Dependencies”Several interfaces exist in Abstractions specifically to break circular dependencies between modules:
| Interface | Cycle Broken |
|---|---|
ICacheStack, CacheEntryOptions, CachePriority, CacheCategories | Authorization, Configuration, and Endpoints need caching without depending on Pragmatic.Caching. |
IPragmaticBuilder | Every module adds Use*() extension methods. The interface lives here so modules do not depend on Pragmatic.Composition.Host. |
ICallContext | Actions provides ActionCallContext; Events checks IsInternalCall when dispatching. Without Abstractions, Events would depend on Actions. |
ICurrentUser | Persistence uses it for auditing; Identity implements it. Without Abstractions, Persistence would depend on Identity. |
IClock | Persistence uses it for timestamps; Actions uses it for lifecycle tracking; Authorization uses it for temporal roles. Without Abstractions, all three would need to depend on Pragmatic.Temporal. |
IDomainEvent, IHasDomainEvents | Persistence entities raise events; the Events module dispatches them. Without Abstractions, Persistence would depend on Events. |
What Belongs in Abstractions
Section titled “What Belongs in Abstractions”A type belongs in Pragmatic.Abstractions if it meets all of these criteria:
| Criterion | Rationale |
|---|---|
| Consumed by two or more modules that must not depend on each other | Single-module types belong in that module |
| Zero implementation logic (or trivially minimal null-object singletons) | Implementation belongs in the module that provides runtime behavior |
| No dependency on ASP.NET Core, EF Core, or heavy external libraries | Abstractions must remain lightweight |
| Represents a stable contract that changes infrequently | Volatile types force cascading updates across all modules |
Types that DO belong
Section titled “Types that DO belong”| Type | Why |
|---|---|
ICurrentUser | Used by Persistence (auditing), Actions (authorization), Endpoints (permissions), Logging (correlation) |
IClock | Used by Persistence (timestamps), Actions (lifecycle), Events (OccurredAt), Authorization (temporal roles) |
IError | Used by Result, Actions, Endpoints, and the Source Generator |
[Service] attribute | Used by the SG at compile time and by any module registering services |
ITenantContext | Used by Persistence (tenant filter), Caching (tenant-scoped keys), Configuration (tenant overrides) |
| Telemetry tag constants | Used by every module that instruments with OpenTelemetry |
Types that DO NOT belong
Section titled “Types that DO NOT belong”| Type | Where It Belongs | Why |
|---|---|---|
EfRepository<T, TId> | Pragmatic.Persistence.EFCore | EF Core implementation detail |
MutationInvoker<T> | Pragmatic.Actions | Actions runtime logic |
ClaimsPrincipalUserAccessor | Pragmatic.Identity.AspNetCore | Depends on ASP.NET Core’s ClaimsPrincipal |
InMemoryEventDispatcher | Pragmatic.Events | Implementation of IDomainEventDispatcher |
WildcardMatcher | Pragmatic.Authorization | Authorization-specific utility logic |
HybridCacheStack | Pragmatic.Caching | Wraps a third-party caching library |
Checklist: Adding a New Type
Section titled “Checklist: Adding a New Type”Before adding a type to this package:
- It is consumed by at least two modules that should not depend on each other.
- It has no implementation logic (or only trivial null-object logic).
- It does not require ASP.NET Core, EF Core, or other heavy dependencies.
- It represents a stable contract unlikely to change frequently.
- Its namespace follows the folder structure under
Pragmatic.*. - If it is an interface with a natural “empty” state, a null-object singleton is provided.
- If it is a marker interface (like
IPermission), it uses static abstract members. - If it is an attribute, it follows the generic-first convention (
[Attr<T>]over[Attr(typeof(T))]).
Versioning
Section titled “Versioning”Because every module in the ecosystem depends on Abstractions:
- Breaking changes cascade to all modules. Avoid them unless absolutely necessary.
- New interfaces are additive (non-breaking). Modules only consume the interfaces they need.
- New members on existing interfaces should use default interface implementations when possible.
- Deprecation via
[Obsolete]with a migration path (seeDependsOnAttributefor an example).
The package follows the same version as the Pragmatic.Design ecosystem. All packages are versioned together.
See Also
Section titled “See Also”- interfaces.md — Complete member-level signatures for every type
- design-principles.md — Detailed dependency rules and decision framework
- common-mistakes.md — Common mistakes when working with Abstractions
- troubleshooting.md — Problem/solution guide and FAQ