Framework Core Integration
PearDrop provides a set of core extension methods and abstractions for initializing the framework, managing databases, and implementing read models.
Server-Side Registration
TryAddPearDrop
Initialize framework core services for the server application.
var builder = WebApplicationBuilder.CreateBuilder(args);
// Framework core bootstrap
builder.Services.TryAddPearDrop(builder.Configuration);
What it registers:
- MediatR pipeline with authorization and validation behaviors
- Default tenant identifier (single-tenant: well-known GUID)
- IClock abstraction for
DateTime.UtcNow - ICommandStore for command event sourcing
- ICommandRunner and IQueryRunner (CQRS facades)
- Logging and monitoring infrastructure
DbContext Factory Registration
Configure how your DbContext connects to the database.
services.AddPearDropSqlServerDbContextFactory<MyAppDbContext>(
"MyApp.Core", // Migrations assembly name (for EF migrations)
"__EFMigrationsHistory", // Migrations history table
"PearDrop"); // Connection string name (optional, defaults to PearDrop)
Connection String Resolution Order:
ConnectionStrings:PearDrop-{ModuleName}(e.g., "PearDrop-Auth", "PearDrop-Files")ConnectionStrings:PearDrop(default fallback)PearDrop:modules:core:PrimaryConnectionString(deprecated legacy fallback)
Example appsettings.json:
{
"ConnectionStrings": {
"PearDrop": "Server=localhost,1440;Database=MyApp;User Id=sa;Password=...;",
"PearDrop-Auth": "Server=localhost,1440;Database=MyApp;User Id=sa;Password=...;",
"PearDrop-Files": "Server=localhost,1440;Database=MyApp;User Id=sa;Password=..."
}
}
Request Tracing and Event Dispatch
// Enable distributed tracing for commands/queries
services.AddCommandTracing();
services.AddQueryTracing();
// Domain and integration event handlers
services.AddDomainEventDispatcher();
services.AddIntegrationEventDispatcher();
Client-Side Registration
TryAddPearDropClient
Register CQRS client services for Blazor WebAssembly applications.
builder.Services.TryAddPearDropClient(builder.HostEnvironment.BaseAddress);
What it registers:
- ICommandRunner for sending commands to the server
- IQueryRunner for executing server-side read operations
- HttpClient configured to call the server API
- BluQube JSON converters for CQRS contract serialization
Authorization Setup
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider>(sp =>
new ServerAuthenticationStateProvider(httpClient));
// For resource-based authorization
builder.Services.AddScoped<IAuthorizationPolicyProvider, AuthPolicyProvider>();
Read-Model Base Abstractions
PearDrop provides base classes for implementing queryable read models without exposing DbContext directly.
IModuleReadModels Interface
Every module that provides read access must implement a read models interface:
/// <summary>
/// Provides queryable read model access for this module.
/// All queries go through this interface — DbContext is never exposed.
/// </summary>
public interface IMyModuleReadModels : IModuleReadModels
{
IReadModelQueryable<EquipmentProjection> Equipment { get; }
IReadModelQueryable<MaintenanceProjection> Maintenance { get; }
}
IReadModelQueryable<T>
Wraps DbSet<T> to provide safe, tenant-isolated QueryAsync operations:
// Inject the interface, not the DbContext
public sealed class GetEquipmentQueryHandler(
IMyModuleReadModels readModels)
{
public async Task<QueryResult<EquipmentDto>> Handle(
GetEquipmentByIdQuery request,
CancellationToken cancellationToken)
{
// Returns IQueryable<T> with tenant isolation applied
var equipment = await readModels.Equipment
.Where(e => e.Id == request.Id)
.Select(e => new EquipmentDto(e.Id, e.Name, e.Category))
.FirstOrDefaultAsync(cancellationToken);
if (equipment == null)
{
return QueryResult<EquipmentDto>.Failed(
ErrorCodes.NotFound, "Equipment not found");
}
return QueryResult<EquipmentDto>.Succeeded(equipment);
}
}
ModuleReadModelsBase<T>
Base implementation for module read models:
public sealed class MyModuleReadModels :
ModuleReadModelsBase<MyReadDbContext>,
IMyModuleReadModels
{
private readonly Lazy<IReadModelQueryable<EquipmentProjection>> equipment;
private readonly Lazy<IReadModelQueryable<MaintenanceProjection>> maintenance;
public MyModuleReadModels(
IDbContextFactory<MyReadDbContext> dbContextFactory,
IMultiTenantContextAccessor tenantContextAccessor,
IReadModelExpressionValidator expressionValidator)
: base(dbContextFactory, tenantContextAccessor, expressionValidator)
{
// Lazy initialization defers DbContext creation
this.equipment = new Lazy<IReadModelQueryable<EquipmentProjection>>(() =>
this.CreateQueryable<EquipmentProjection>(new ReadModelMetadata
{
EntityName = nameof(EquipmentProjection),
RequiresTenantIsolation = true,
MaxTakeSize = 1000,
AllowedIncludes = new HashSet<string>
{
nameof(EquipmentProjection.Category),
}
}));
this.maintenance = new Lazy<IReadModelQueryable<MaintenanceProjection>>(() =>
this.CreateQueryable<MaintenanceProjection>(new ReadModelMetadata
{
EntityName = nameof(MaintenanceProjection),
RequiresTenantIsolation = true,
MaxTakeSize = 1000,
}));
}
public override string ModuleName => "MyModule";
public IReadModelQueryable<EquipmentProjection> Equipment => this.equipment.Value;
public IReadModelQueryable<MaintenanceProjection> Maintenance => this.maintenance.Value;
}
Key Benefits:
- DbContext stays internal to the module
- Automatic tenant isolation filtering
- Safe QueryAsync operations with expression validation
- Lazy initialization for performance
Multi-Tenancy Support
Single-Tenant Mode
In single-tenant deployments, the framework uses a well-known GUID for all operations:
// framework default
SingleTenantIdentifier.CurrentTenantId = "00000000-0000-0000-0000-000000000001"
Switch between single-tenant and multi-tenant without changing application code—both use the same GUID-based tenant ID pattern.
Tenant Context Access
// In handlers or services
public sealed class MyQueryHandler
{
private readonly IMultiTenantContextAccessor tenantContextAccessor;
public async Task<QueryResult<MyDto>> Handle(MyQuery request, CancellationToken ct)
{
var tenantId = tenantContextAccessor.TenantId; // Current tenant GUID
// Use in queries
var data = await readModels.MyEntities
.Where(x => x.TenantId == tenantId)
.ToListAsync(ct);
}
}
Extension Points
Custom Authorization
See Security & Authorization Patterns for command/query authorization.
Custom Events
- Domain Events for transactional consistency within a bounded context
- Integration Events (DotNetCore.CAP) for cross-module eventual consistency
Custom Validators
Use FluentValidation for command/query input validation:
public sealed class CreateTaskCommandValidator : AbstractValidator<CreateTaskCommand>
{
public CreateTaskCommandValidator()
{
RuleFor(x => x.Title).NotEmpty().MaximumLength(200);
RuleFor(x => x.Description).MaximumLength(2000);
}
}
Related Topics
- Data Persistence & Read Models — DbContext patterns and data management
- Security & Authorization Patterns — Authorization requirements and command/query security
- Multi-Tenancy — Tenant isolation and resolution strategies