Architecture Overview
Understanding how PearDrop applications are structured.
High-Level Architecture
┌─────────────────────────────────────────────────────────┐
│ Blazor WebAssembly Client (App.Client) │
│ - Interactive components │
│ - Send Commands via ICommandRunner │
│ - Execute Queries via IQueryRunner │
└──────────────────────┬──────────────────────────────────┘
│ HTTPS
│ BluQube API Contracts
↓
┌─────────────────────────────────────────────────────────┐
│ Server Application (App) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Write Path (Commands) │ │
│ │ ─────────────────────────────────────────────── │ │
│ │ 1. Validate input (FluentValidation) │ │
│ │ 2. Load aggregate from repository │ │
│ │ 3. Execute business logic │ │
│ │ 4. Raise domain events (immediate) │ │
│ │ 5. Save changes → AppDbContext │ │
│ │ 6. Publish integration events (async, CAP) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Read Path (Queries) │ │
│ │ ─────────────────────────────────────────────── │ │
│ │ 1. Fetch from read models (AppReadModels) │ │
│ │ 2. Apply filters and pagination │ │
│ │ 3. Return projection DTOs │ │
│ │ (No domain logic, optimized for reads) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└──────────────┬───────────────────────────────────────┬─┘
│ │
Write Model Read Model
AppDbContext AppReadDbContext
│ │
└───────────────→┌─────────────────────┘
SQL Server
(Single DB)
Key Architectural Concepts
Command Query Responsibility Segregation (CQRS)
PearDrop strictly separates writes from reads:
Write Side (Commands):
- Domain aggregates with business logic
- Command handlers that validate and execute
- Single database transaction per command
- Change tracking enabled
Read Side (Queries):
- Projection models optimized for reading
- Read-only DbContext mapped to database views
- No domain logic (pure projection)
- High-performance queries
Benefits:
- ✅ Write logic stays in aggregates (business rules protected)
- ✅ Read queries are simple and fast (denormalized)
- ✅ Scalable - can optimize each side independently
- ✅ Testable - tests don't cross concerns
Domain-Driven Design (DDD)
PearDrop is structured around DDD patterns:
Aggregates
- Domain entities that protect business invariants
- Example:
EquipmentAggregatemanages equipment and its state - Encapsulate all business rules related to that entity
Bounded Contexts
- Groups of related aggregates organized in folders
- Example:
EquipmentAggregate/is a context folder - Clear boundaries prevent spaghetti code
Commands & Queries
- Commands represent intent: "CreateEquipment", "UpdateEquipment"
- Queries represent requests for data: "GetEquipmentById", "ListEquipment"
- Handlers execute the logic
Layered Architecture
┌──────────────────────────────┐
│ Presentation (Components) │ ← User interactions
│ ├─ Blazor Components │
│ └─ API Controllers │
├──────────────────────────────┤
│ Application (Handlers) │ ← Use cases
│ ├─ Command Handlers │
│ ├─ Query Handlers │
│ └─ Validators │
├──────────────────────────────┤
│ Domain (Aggregates) │ ← Business logic
│ ├─ Aggregates │
│ ├─ Value Objects │
│ ├─ Domain Events │
│ └─ Specifications │
├──────────────────────────────┤
│ Infrastructure (Data Access)│ ← Technical details
│ ├─ DbContexts │
│ ├─ Repositories │
│ ├─ External Services │
│ └─ Persistence │
└──────────────────────────────┘
Dependency Rule:
- Inner layers don't depend on outer layers
- Aggregates (Domain) → Commands/Queries (Application)
- Nothing depends directly on Infrastructure
Project Organization
YourProject.App/ ASP.NET Core Server (API only)
├─ Infrastructure/
│ ├─ Domain/ Business logic (aggregates, commands)
│ ├─ Queries/ Read-side handlers
│ └─ Data/ Database contexts
└─ Program.cs
YourProject.App.Client/ Blazor WebAssembly Client
├─ Infrastructure/
│ └─ Module.cs [BluQubeRequester] services
├─ Components/ Blazor components (Pages, Layouts, Shared)
└─ Program.cs
Data Flow: A Complete Example
Here's how a Create Note command flows through the system:
1. User Interaction
// Component sends command
var result = await commandRunner.Send(
new CreateNoteCommand("My Note", "Content here"));
2. Server Receives Command
Command → BluQube API endpoint → ICommandRunner.Send()
3. Command Handler Executes
public class CreateNoteCommandHandler : AuditableCommandHandler<CreateNoteCommand, Guid>
{
// 1. Validate (FluentValidation)
// 2. Create aggregate
var note = new NoteAggregate(Guid.NewGuid(), request.Title, request.Content, userId);
// 3. Add to repository
repository.Add(note);
// 4. Save to WriteModel DbContext
repository.UnitOfWork.SaveEntitiesAsync();
// 5. Publish integration events (async)
publisher.PublishAsync(new NoteCreatedIntegrationEvent(...));
}
4. Database Updated
AppDbContext (WriteModel)
└─ Note table updated
5. Client Displays Data
// Component queries for new note
var result = await queryRunner.Send(
new GetNoteByIdQuery(noteId));
// Handler fetches from read model
IAppReadModels.Notes.Where(n => n.Id == noteId).ToListAsync();
6. Response to Client
Query Handler → DTO → Client → Component displays
Technology Stack
- Framework: .NET 10
- Frontend: Blazor WebAssembly
- Database: SQL Server (EF Core)
- CQRS: BluQube (source generators)
- Validation: FluentValidation
- Messaging: DotNetCore.CAP (integration events)
- Authorization: MediatR.Behaviors.Authorization
- Testing: xUnit, Fixture builders
Key Principles
- Single Responsibility - Aggregates handle business logic, handlers orchestrate
- Domain-Centric - Business rules live in domain, not in handlers or components
- CQRS Separation - Writes and reads are completely separate concerns
- Immutable Read Models - Queries never modify data
- Event-Driven - Domain events for consistency, integration events for scale
- Repository Pattern - Data access abstracted, testable without DB
- Lazy Initialization - Read models initialized on-demand for performance
Next Steps
- CQRS Pattern - Deep dive into Commands and Queries
- Your First Feature - Build a complete example