Skip to content

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.

  • .NET 10 SDK installed
  • A Pragmatic.Design project (or a new .NET project)

Terminal window
dotnet add package Pragmatic.Abstractions

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


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.


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.

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

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));
}
}
using Pragmatic.MultiTenancy;
public class TenantAwareService(ITenantContext tenant)
{
public string GetCacheKey(string key)
=> tenant.IsResolved ? $"{tenant.TenantId}:{key}" : key;
}
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.

using Pragmatic.Composition.Attributes;
[Module(Name = "Booking", Version = "1.0")]
public class BookingModule;
using Pragmatic.Composition.Attributes;
[Service<IPaymentGateway>(Lifetime = ServiceLifetime.Scoped)]
public class StripePaymentGateway : IPaymentGateway
{
// The SG registers this in DI automatically
}
using Pragmatic.Authorization;
[RequirePermission("booking.reservation.create")]
public partial class CreateReservationAction : DomainAction<Guid>
{
// The SG generates permission enforcement in the pipeline
}

Abstractions provides safe defaults for optional dependencies:

// Anonymous user -- all permission checks return false
ICurrentUser anonymous = AnonymousUser.Instance;
anonymous.IsAuthenticated; // false
anonymous.Authorization.HasPermission("anything"); // false
// Unresolved tenant -- no tenant context
ITenantContext unresolved = UnresolvedTenantContext.Instance;
unresolved.IsResolved; // false
unresolved.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 metadata
IAuthenticationContext nullAuth = NullAuthenticationContext.Instance;
nullAuth.Scheme; // null
nullAuth.Issuer; // null

In the host project (the one that actually runs), add the runtime packages and let the source generator connect everything:

Program.cs
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.


// Domain module -- references only Pragmatic.Abstractions
public 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
}
[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...
}
// Development: in-memory everything
services.AddSingleton<IFeatureFlagStore, InMemoryFeatureFlagStore>();
services.AddSingleton<IConfigurationStore, InMemoryConfigurationStore>();
// Production: real backends
services.AddSingleton<IFeatureFlagStore, LaunchDarklyFeatureFlagStore>();
services.AddSingleton<IConfigurationStore, AzureAppConfigurationStore>();

The domain code never changes. Only the composition root (host project) differs.


Belongs in AbstractionsBelongs 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