Getting Started with Pragmatic.Abstractions
This guide walks you through referencing Pragmatic.Abstractions and using its interfaces in your code. By the end, you will understand the layer model, know which interfaces to use for common scenarios, and be able to wire up implementations via DI.
Prerequisites
Section titled “Prerequisites”- .NET 10 SDK installed
- A Pragmatic.Design project (or a new .NET project)
Step 1: Add the Package Reference
Section titled “Step 1: Add the Package Reference”dotnet add package Pragmatic.AbstractionsOr in your .csproj:
<PackageReference Include="Pragmatic.Abstractions" />This gives you access to all shared interfaces, attributes, and contracts. No ASP.NET Core or EF Core dependencies are pulled in.
Step 2: Understand the Layer Model
Section titled “Step 2: Understand the Layer Model”Pragmatic.Design uses a strict layering model. Abstractions sits at Layer 0 — the foundation that every other module depends on.
Layer 0 (Foundation) You are here: Pragmatic.Abstractions |Layer 1 (Capabilities) Validation, Mapping, Specification, Caching, FeatureFlags, ... |Layer 2 (Integration) Actions, Endpoints, Persistence.EFCore, Composition, ...Rule: Lower layers never reference higher layers. Abstractions never references Actions, Persistence, or any Layer 1/2 module. This prevents circular dependencies.
Consequence: When you write domain code that depends on IRepository<T, TId>, you reference Abstractions for the interface. The actual EF Core implementation is wired up at composition time in the host project. Your domain code never knows about EF Core.
Step 3: Use Interfaces in Domain Code
Section titled “Step 3: Use Interfaces in Domain Code”Repository Access
Section titled “Repository Access”using Pragmatic.Persistence.Repository;
public class InvoiceService( IReadRepository<Invoice, Guid> invoices, IRepository<Invoice, Guid> repository){ public async Task<Invoice?> GetByIdAsync(Guid id, CancellationToken ct) => await invoices.GetByIdAsync(id, ct);
public void Add(Invoice invoice) => repository.Add(invoice);}The IReadRepository and IRepository interfaces are defined in Abstractions. The EF Core implementation is registered by the source generator when Pragmatic.Persistence.EFCore is referenced in the host project.
Current User
Section titled “Current User”using Pragmatic.Identity;
public class OrderService(ICurrentUser currentUser){ public async Task<Result<Order, ForbiddenError>> CreateOrder(CreateOrderRequest request) { if (!currentUser.Authorization.HasPermission("orders.create")) return new ForbiddenError();
// currentUser.Id, currentUser.TenantId, etc. }}ICurrentUser is defined in Abstractions. In HTTP contexts, Pragmatic.Identity.AspNetCore provides ClaimsPrincipalUserAccessor. In background jobs, use SystemUser.Instance.
using Pragmatic.Temporal.Clock;
public class ReservationService(IClock clock){ public bool IsExpired(Reservation reservation) => reservation.ExpiresAt < clock.UtcNow;}IClock is defined in Abstractions. Pragmatic.Temporal provides SystemClock (production) and TestClock (unit tests).
Domain Events
Section titled “Domain Events”using Pragmatic.Events;
public class PaymentService(IDomainEventDispatcher events){ public async Task ProcessPaymentAsync(Payment payment, CancellationToken ct) { // ... payment logic ... await events.DispatchAsync(new PaymentCompleted(payment.Id, payment.Amount)); }}Tenant Context
Section titled “Tenant Context”using Pragmatic.MultiTenancy;
public class TenantAwareService(ITenantContext tenant){ public string GetCacheKey(string key) => tenant.IsResolved ? $"{tenant.TenantId}:{key}" : key;}Caching
Section titled “Caching”using Pragmatic.Caching;
public class ProductService(ICacheStack cache){ public async Task<Product?> GetProductAsync(Guid id, CancellationToken ct) { return await cache.GetOrSetAsync( $"product:{id}", async _ => await LoadFromDatabase(id, ct), CacheEntryOptions.WithDuration(TimeSpan.FromMinutes(10))); }}Step 4: Use Attributes for Source Generation
Section titled “Step 4: Use Attributes for Source Generation”Abstractions defines the attributes that the source generator reads at compile time.
Module Declaration
Section titled “Module Declaration”using Pragmatic.Composition.Attributes;
[Module(Name = "Booking", Version = "1.0")]public class BookingModule;Service Registration
Section titled “Service Registration”using Pragmatic.Composition.Attributes;
[Service<IPaymentGateway>(Lifetime = ServiceLifetime.Scoped)]public class StripePaymentGateway : IPaymentGateway{ // The SG registers this in DI automatically}Permission Declaration
Section titled “Permission Declaration”using Pragmatic.Authorization;
[RequirePermission("booking.reservation.create")]public partial class CreateReservationAction : DomainAction<Guid>{ // The SG generates permission enforcement in the pipeline}Step 5: Use Null-Object Singletons
Section titled “Step 5: Use Null-Object Singletons”Abstractions provides safe defaults for optional dependencies:
// Anonymous user -- all permission checks return falseICurrentUser anonymous = AnonymousUser.Instance;anonymous.IsAuthenticated; // falseanonymous.Authorization.HasPermission("anything"); // false
// Unresolved tenant -- no tenant contextITenantContext unresolved = UnresolvedTenantContext.Instance;unresolved.IsResolved; // falseunresolved.TenantId; // null
// Full access -- all permission checks return true (for system/background contexts)IUserAuthorization fullAccess = FullAccessUserAuthorization.Instance;fullAccess.HasPermission("anything"); // true
// Null authentication -- no auth metadataIAuthenticationContext nullAuth = NullAuthenticationContext.Instance;nullAuth.Scheme; // nullnullAuth.Issuer; // nullStep 6: Wire Up Implementations
Section titled “Step 6: Wire Up Implementations”In the host project (the one that actually runs), add the runtime packages and let the source generator connect everything:
await PragmaticApp.RunAsync(args, app =>{ // Identity (provides ICurrentUser) app.UseAuthentication<NoOpAuthenticationHandler>("PragmaticDefault");
// Multi-tenancy (provides ITenantContext) app.UseMultiTenancy(mt => mt.UseHeader());
// Storage (provides IFileStorage) app.UseStorage(sp => new LocalDiskFileStorage( app.Environment.WebRootPath, sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));});The source generator reads [Module], [Include<T>], [PragmaticDatabase] attributes and generates the DI wiring automatically. Most interfaces are registered without manual services.Add*() calls.
Common Patterns
Section titled “Common Patterns”Depend on Interfaces, Not Implementations
Section titled “Depend on Interfaces, Not Implementations”// Domain module -- references only Pragmatic.Abstractionspublic class BookingService( IRepository<Reservation, Guid> reservations, // Interface ICurrentUser currentUser, // Interface IClock clock) // Interface{ // No reference to EF Core, ASP.NET Core, or any runtime package}Test with Mocks
Section titled “Test with Mocks”[Fact]public async Task CreateReservation_SetsCreatedBy(){ var user = Substitute.For<ICurrentUser>(); user.Id.Returns("user-42");
var clock = Substitute.For<IClock>(); clock.UtcNow.Returns(new DateTimeOffset(2026, 3, 27, 12, 0, 0, TimeSpan.Zero));
var repo = Substitute.For<IRepository<Reservation, Guid>>(); var service = new BookingService(repo, user, clock);
// Act and assert...}Switch Implementations per Environment
Section titled “Switch Implementations per Environment”// Development: in-memory everythingservices.AddSingleton<IFeatureFlagStore, InMemoryFeatureFlagStore>();services.AddSingleton<IConfigurationStore, InMemoryConfigurationStore>();
// Production: real backendsservices.AddSingleton<IFeatureFlagStore, LaunchDarklyFeatureFlagStore>();services.AddSingleton<IConfigurationStore, AzureAppConfigurationStore>();The domain code never changes. Only the composition root (host project) differs.
What Belongs in Abstractions vs. Modules
Section titled “What Belongs in Abstractions vs. Modules”| Belongs in Abstractions | Belongs in a Module |
|---|---|
Interface definitions (IRepository, IClock) | Concrete implementations (EfRepository, SystemClock) |
Attribute definitions ([Service], [Module]) | Source generator that reads the attributes |
Enums (PrincipalKind, DatabaseProvider) | Runtime logic (middleware, interceptors) |
Null-object singletons (AnonymousUser) | Full implementations (ClaimsPrincipalUserAccessor) |
Record types for cross-module data (PermissionInfo) | Business logic classes |
Next Steps
Section titled “Next Steps”- Read Concepts for the full architecture and interface catalog
- Read Interfaces for member-level documentation of every interface
- Read Design Principles for the rules governing what goes into Abstractions
- Browse the Pragmatic.Design README for the full module catalog