Skip to main content

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:

  1. ConnectionStrings:PearDrop-{ModuleName} (e.g., "PearDrop-Auth", "PearDrop-Files")
  2. ConnectionStrings:PearDrop (default fallback)
  3. 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);
}
}