Skip to content

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.


A modular framework needs shared contracts. Without a central place for those contracts, two bad things happen.

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.

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 Core
public 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 HttpContext cannot run in a gRPC host, a message handler, or a Lambda function.
  • Testing requires mocking framework types (ClaimsPrincipal, HttpContext) instead of simple interfaces.

Without a shared package, every module that needs “the current user” would define its own interface:

// In Pragmatic.Persistence
public interface IAuditUser { string? Id { get; } }
// In Pragmatic.Actions
public interface IActionUser { string Id { get; } bool HasPermission(string p); }
// In Pragmatic.Events
public 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.


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:

DependencyPurpose
Pragmatic.SpecificationSpecification<T> base class used by IReadRepository
Microsoft.Extensions.Configuration.AbstractionsIConfiguration for IPragmaticBuilder
Microsoft.Extensions.DependencyInjection.AbstractionsIServiceCollection for IPragmaticBuilder and Decorate extensions
Microsoft.Extensions.Hosting.AbstractionsIHostEnvironment 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.

  1. No circular dependencies. Persistence and Identity both reference Abstractions, not each other.
  2. Framework independence. Domain modules depend on ICurrentUser, not ClaimsPrincipal. The ASP.NET Core adapter lives in Pragmatic.Identity.AspNetCore and is only referenced by the host.
  3. Single interface per concept. One ICurrentUser, one IClock, one IRepository. Every module consumes the same contract. One DI registration satisfies all consumers.
  4. Lightweight referencing. A console app, test harness, or background worker can reference Abstractions without pulling ASP.NET Core or EF Core.

The catalog below organizes every public type by domain. For full member-level signatures, see interfaces.md.

TypeKindNamespace
IErrorInterfacePragmatic.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.


TypeKindNamespace
IEntityInterfacePragmatic.Persistence.Entity
IEntity<TId>InterfacePragmatic.Persistence.Entity
IAuditableInterfacePragmatic.Persistence.Entity
ISoftDeleteInterfacePragmatic.Persistence.Entity
IChangeTrackingInterfacePragmatic.Persistence.Entity
ITemporalRelationInterfacePragmatic.Persistence.Entity
IEntityFactory<TEntity>InterfacePragmatic.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.


TypeKindNamespace
IReadRepository<TEntity, TId>InterfacePragmatic.Persistence.Repository
IRepository<TEntity, TId>InterfacePragmatic.Persistence.Repository
IUnitOfWorkInterfacePragmatic.Persistence.Repository
ITransactionInterfacePragmatic.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.


TypeKindNamespace
ICurrentUserInterfacePragmatic.Identity
IAuthenticationContextInterfacePragmatic.Identity
IUserProfileInterfacePragmatic.Identity
PrincipalKindEnumPragmatic.Identity
AnonymousUserSealed classPragmatic.Identity
NullAuthenticationContextSealed classPragmatic.Identity
CurrentUserExtensionsStatic classPragmatic.Identity
[PragmaticUser]AttributePragmatic.Identity
[ProfileProperty]AttributePragmatic.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).


TypeKindNamespace
IUserAuthorizationInterfacePragmatic.Authorization
IPermissionInterfacePragmatic.Authorization
IRoleInterfacePragmatic.Authorization
IGroupInterfacePragmatic.Authorization
IRoleDefinitionInterfacePragmatic.Authorization
IPermissionProviderInterfacePragmatic.Authorization
IPermissionCheckerInterfacePragmatic.Authorization
IResourceAuthorizer<TResource>InterfacePragmatic.Authorization
PermissionInfoRecordPragmatic.Authorization
RoleInfoRecordPragmatic.Authorization
NullUserAuthorizationSealed classPragmatic.Authorization
FullAccessUserAuthorizationSealed classPragmatic.Authorization
[RequirePermission]AttributePragmatic.Authorization
[RequireAnyPermission]AttributePragmatic.Authorization
[ExplicitPermission] / [ExplicitPermission<T>]AttributePragmatic.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).


TypeKindNamespace
IDomainEventInterfacePragmatic.Events
IDomainEventDispatcherInterfacePragmatic.Events
IDomainEventHandler<TEvent>InterfacePragmatic.Events
IHasDomainEventsInterfacePragmatic.Events
EntityPropertyChanged<TEntity>RecordPragmatic.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.


TypeKindNamespace
IClockInterfacePragmatic.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.


TypeKindNamespace
ITenantContextInterfacePragmatic.MultiTenancy
ITenantResolverInterfacePragmatic.MultiTenancy
ITenantEntityInterfacePragmatic.MultiTenancy
UnresolvedTenantContextSealed classPragmatic.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).


TypeKindNamespace
ICacheStackInterfacePragmatic.Caching
CacheEntryOptionsClassPragmatic.Caching
CachePriorityEnumPragmatic.Caching
CacheCategoriesStatic classPragmatic.Caching
CacheCategories.DefaultClassPragmatic.Caching
CacheCategories.OutputCacheClassPragmatic.Caching
CacheCategories.RateLimitingClassPragmatic.Caching
CacheCategories.PermissionsClassPragmatic.Caching
CacheCategories.ConfigurationClassPragmatic.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.


TypeKindNamespace
IConfigurationStoreInterfacePragmatic.Configuration
ISecretStoreInterfacePragmatic.Configuration
ConfigurationChangeRecordPragmatic.Configuration
EnvironmentProfileClassPragmatic.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.


TypeKindNamespace
IFeatureFlagInterfacePragmatic.FeatureFlags
IFeatureFlagStoreInterfacePragmatic.FeatureFlags
IFeatureFlagContextProviderInterfacePragmatic.FeatureFlags
FeatureFlagContextRecordPragmatic.FeatureFlags
FeatureFlagDefinitionRecordPragmatic.FeatureFlags
FeatureFlagRuleRecordPragmatic.FeatureFlags
FeatureFlagChangeRecordPragmatic.FeatureFlags
FeatureFlagStoreExtensionsStatic classPragmatic.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.


TypeKindNamespace
IGlobalizationContextInterfacePragmatic.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).


TypeKindNamespace
ICallContextInterfacePragmatic.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).


TypeKindNamespace
IPragmaticBuilderInterfacePragmatic.Composition
IPackageDefinitionInterfacePragmatic.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.


AttributeTargetNamespace
[Module]ClassPragmatic.Composition.Attributes
[Service] / [Service<TInterface>]ClassPragmatic.Composition.Attributes
[Decorator]ClassPragmatic.Composition.Attributes
[Inject]Property/MethodPragmatic.Composition.Attributes
[ServiceFactory] / [Factory]Class/MethodPragmatic.Composition.Attributes
[Include<TModule>]ClassPragmatic.Composition.Attributes
[Include<TModule, TDatabase>]ClassPragmatic.Composition.Attributes
[Include<TModule, TDatabase, TDbContext>]ClassPragmatic.Composition.Attributes
[IncludeModule<TModule>]ClassPragmatic.Composition.Attributes
[DependsOn<TModule>]Class (deprecated)Pragmatic.Composition.Attributes
[StartupStep]ClassPragmatic.Composition.Attributes
[PragmaticDatabase]ClassPragmatic.Composition.Attributes
[RequiresConfig]ClassPragmatic.Composition.Attributes
[UsePackage<TPackage>]ClassPragmatic.Composition.Attributes
[RemoteBoundary<TModule>]ClassPragmatic.Composition.Attributes
[PragmaticMetadata]AssemblyPragmatic.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).

TypeNamespaceDescription
ServiceLifetimePragmatic.Composition.AttributesSingleton, Scoped, Transient
DatabaseProviderPragmatic.Composition.EnumsSqlServer, PostgreSql, SQLite, MySql, InMemory
MetadataCategoryPragmatic.Composition.Metadata15 categories for SG-generated assembly metadata
PragmaticDatabasePragmatic.Composition.DatabaseAbstract base class for database declarations

TypeNamespace
ServiceCollectionDecorateExtensionsPragmatic.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.


TypeKindNamespace
ActivityHelperStatic classPragmatic.Telemetry
TelemetryOptionsClassPragmatic.Telemetry

ActivityHelper provides extension methods for System.Diagnostics.Activity: RecordException, SetSuccess, SetFailure, AddNamedEvent. TelemetryOptions configures OpenTelemetry: Enabled, Tracing, Metrics, Logging, UseOtlpExporter, ServiceName, SamplingRatio.


Standardized tag name constants for OpenTelemetry instrumentation across all modules.

ClassPrefixNamespace
ErrorTagserror.*, exception.*Pragmatic.Telemetry.Conventions
ActionTagspragmatic.action.*, pragmatic.mutation.*Pragmatic.Telemetry.Conventions
DbTagsdb.*, pragmatic.db.*Pragmatic.Telemetry.Conventions
CacheTagscache.*, pragmatic.cache.*Pragmatic.Telemetry.Conventions
ResilienceTagspragmatic.resilience.*Pragmatic.Telemetry.Conventions
EventTagspragmatic.event.*Pragmatic.Telemetry.Conventions
I18NTagspragmatic.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.


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
└── Specification

Layer 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.


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.

Abstractions must never reference Microsoft.EntityFrameworkCore. Repository interfaces (IRepository<T, TId>) are defined here; their EF Core implementations live in Pragmatic.Persistence.EFCore.

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 on System.Diagnostics.Activity.

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
}

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))].

For interfaces consumed in contexts where no real implementation may exist (anonymous requests, no-tenant scenarios), Abstractions provides singleton null-objects:

Null ObjectInterfaceBehavior
AnonymousUserICurrentUserId = empty, IsAuthenticated = false, all authorization = false
NullAuthenticationContextIAuthenticationContextAll properties = null/false
NullUserAuthorizationIUserAuthorizationAll checks return false, all collections empty
FullAccessUserAuthorizationIUserAuthorizationAll checks return true (system-level contexts)
UnresolvedTenantContextITenantContextTenantId = 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.

Attributes in Abstractions follow these rules:

  1. Generic over typeof: Always [Attr<T>] over [Attr(typeof(T))].
  2. Minimal properties: Only properties that affect SG output or runtime behavior.
  3. No implementation logic: Attributes are pure data carriers.
  4. Inherited = false: Most attributes are not inherited (each type opts in explicitly).
  5. AllowMultiple only when needed: [Include<T>] allows multiple (a host may include many modules). Most other attributes are single-use.

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.


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 (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.

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 can reference Abstractions directly to mock interfaces without importing runtime modules:

// Test can mock ICurrentUser without referencing Pragmatic.Identity.AspNetCore
var mockUser = Substitute.For<ICurrentUser>();
mockUser.Id.Returns("test-user-123");
mockUser.IsAuthenticated.Returns(true);

Several interfaces exist in Abstractions specifically to break circular dependencies between modules:

InterfaceCycle Broken
ICacheStack, CacheEntryOptions, CachePriority, CacheCategoriesAuthorization, Configuration, and Endpoints need caching without depending on Pragmatic.Caching.
IPragmaticBuilderEvery module adds Use*() extension methods. The interface lives here so modules do not depend on Pragmatic.Composition.Host.
ICallContextActions provides ActionCallContext; Events checks IsInternalCall when dispatching. Without Abstractions, Events would depend on Actions.
ICurrentUserPersistence uses it for auditing; Identity implements it. Without Abstractions, Persistence would depend on Identity.
IClockPersistence 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, IHasDomainEventsPersistence entities raise events; the Events module dispatches them. Without Abstractions, Persistence would depend on Events.

A type belongs in Pragmatic.Abstractions if it meets all of these criteria:

CriterionRationale
Consumed by two or more modules that must not depend on each otherSingle-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 librariesAbstractions must remain lightweight
Represents a stable contract that changes infrequentlyVolatile types force cascading updates across all modules
TypeWhy
ICurrentUserUsed by Persistence (auditing), Actions (authorization), Endpoints (permissions), Logging (correlation)
IClockUsed by Persistence (timestamps), Actions (lifecycle), Events (OccurredAt), Authorization (temporal roles)
IErrorUsed by Result, Actions, Endpoints, and the Source Generator
[Service] attributeUsed by the SG at compile time and by any module registering services
ITenantContextUsed by Persistence (tenant filter), Caching (tenant-scoped keys), Configuration (tenant overrides)
Telemetry tag constantsUsed by every module that instruments with OpenTelemetry
TypeWhere It BelongsWhy
EfRepository<T, TId>Pragmatic.Persistence.EFCoreEF Core implementation detail
MutationInvoker<T>Pragmatic.ActionsActions runtime logic
ClaimsPrincipalUserAccessorPragmatic.Identity.AspNetCoreDepends on ASP.NET Core’s ClaimsPrincipal
InMemoryEventDispatcherPragmatic.EventsImplementation of IDomainEventDispatcher
WildcardMatcherPragmatic.AuthorizationAuthorization-specific utility logic
HybridCacheStackPragmatic.CachingWraps a third-party caching library

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))]).

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 (see DependsOnAttribute for an example).

The package follows the same version as the Pragmatic.Design ecosystem. All packages are versioned together.