Project Structure
Understanding how a PearDrop project is organized.
Directory Layout
YourProject/
├── source/
│ ├── YourProject.App/ # Server application
│ │ ├── Infrastructure/
│ │ │ ├── Module.cs # DI registration & BluQube setup
│ │ │ ├── Domain/ # Write-side domain
│ │ │ │ └── <AggregateNameAggregate>/
│ │ │ │ ├── AggregateRoot/ # Domain entity with business logic
│ │ │ │ │ └── <Aggregate>.cs
│ │ │ │ ├── Commands/ # Command DTOs
│ │ │ │ │ └── Create<Aggregate>Command.cs
│ │ │ │ ├── CommandHandlers/ # Command execution logic
│ │ │ │ │ └── Create<Aggregate>CommandHandler.cs
│ │ │ │ ├── Events/ # Domain events (optional)
│ │ │ │ └── EventHandlers/ # Domain event handlers
│ │ │ ├── Queries/ # All queries (read-side)
│ │ │ │ ├── Get<Aggregate>ByIdQuery.cs
│ │ │ │ ├── List<Aggregate>sQuery.cs
│ │ │ │ └── QueryHandlers/
│ │ │ │ ├── Get<Aggregate>ByIdQueryHandler.cs
│ │ │ │ └── List<Aggregate>sQueryHandler.cs
│ │ │ └── Data/
│ │ │ ├── WriteModel/
│ │ │ │ └── AppDbContext.cs # RW DbContext (change tracking)
│ │ │ └── ReadModel/
│ │ │ ├── AppReadDbContext.cs # Read-only DbContext
│ │ │ ├── AppReadModels.cs # Lazy-initialized projections
│ │ │ └── ReadModel/
│ │ │ ├── AppReadDbContext.cs # Read-only DbContext
│ │ │ ├── AppReadModels.cs # Lazy-initialized projections
│ │ │ └── Projections/
│ │ │ ├── <Aggregate>Projection.cs
│ │ │ └── ...
│ │ ├── QueryHandlers/ # Server-side query execution
│ │ │ ├── Get<Aggregate>ByIdQueryHandler.cs
│ │ │ └── List<Aggregate>sQueryHandler.cs
│ │ ├── Program.cs # Server startup & configuration
│ │ └── appsettings.*.json
│ │
│ └── YourProject.App.Client/ # Blazor WebAssembly client
│ ├── Infrastructure/
│ │ └── Module.cs # [BluQubeRequester] attribute
│ ├── Queries/ # Client-side query contracts
│ │ ├── Get<Aggregate>ByIdQuery.cs
│ │ └── List<Aggregate>sQuery.cs
│ ├── Components/ # Client-side Blazor components
│ │ ├── Pages/
│ │ ├── Layouts/
│ │ └── Shared/
│ └── Program.cs # WASM client startup
│
├── tests/
│ ├── YourProject.Tests/ # Unit & integration tests
│ │ ├── Fixtures/
│ │ ├── Aggregates/
│ │ ├── Queries/
│ │ ├── Commands/
│ │ └── ...
│ └── ...
│
├── docs/
│ ├── DEVELOPER-ONBOARDING.md # Setup guide (template-managed)
│ ├── GETTING-STARTED.md # Feature dev guide (template-managed)
│ ├── STRUCTURE.md # Project layout details
│ ├── CLI-USAGE.md # PearDrop CLI reference
│ └── ...
│
├── docker-compose.yml # Local services (SQL, Redis)
├── global.json # .NET version
├── nuget.config # NuGet feed configuration
├── peardrop.json # PearDrop CLI config
├── YourProject.sln # Visual Studio solution
├── Directory.Build.props # Shared project settings
└── README.md # Project overview
Key Folders Explained
Infrastructure/Domain
The write-side - encapsulates all business logic.
Each aggregate is organized in a bounded context folder:
Domain/
├── EquipmentAggregate/ # Bounded context (group of related aggregates)
│ ├── AggregateRoot/
│ │ ├── EquipmentAggregate.cs # Domain entity
│ │ ├── Specifications/ # Query specifications (optional)
│ │ └── ...
│ ├── Commands/
│ │ ├── CreateEquipmentCommand.cs
│ │ ├── UpdateEquipmentCommand.cs
│ │ └── ...
│ ├── CommandHandlers/
│ │ ├── CreateEquipmentCommandHandler.cs
│ │ └── ...
│ ├── Events/
│ │ └── EquipmentCreatedDomainEvent.cs (optional)
│ ├── EventHandlers/
│ │ └── EquipmentCreatedEventHandler.cs (optional)
│ └── ...
│
└── CheckoutAggregate/ # Another bounded context
├── AggregateRoot/
│ └── CheckoutAggregate.cs
├── Commands/
│ └── ...
└── ...
Infrastructure/Queries
The read-side - all query operations fetch from read models.
Queries/
├── GetEquipmentByIdQuery.cs # Client-side query contract
├── ListAvailableEquipmentQuery.cs # Client-side query contract
├── SearchEquipmentQuery.cs # Client-side query contract
└── QueryResults/
├── GetEquipmentByIdQueryResult.cs
├── ListAvailableEquipmentQueryResult.cs
└── SearchEquipmentQueryResult.cs
Server-side QueryHandlers (YourProject.App/Infrastructure/QueryHandlers/):
QueryHandlers/
├── GetEquipmentByIdQueryHandler.cs # Server receives query, returns result
├── ListAvailableEquipmentQueryHandler.cs
└── SearchEquipmentQueryHandler.cs
The client sends queries (from WASM), the server handles them (returns read model data). Query DTOs are contracts between client and server.
Infrastructure/Data
Write Model (WriteModel/AppDbContext.cs):
- Change tracking enabled
- Tables for all aggregates
- Used by command handlers during
SaveEntitiesAsync()
Read Model (ReadModel/):
- Read-only focused DbContext
- Mapped to views for performance
- Lazy-initialized queryable properties in
AppReadModels.cs
YourProject.App.Client/Components
Client-side Blazor components (WebAssembly):
File organization:
Components/
├── Layout/
│ ├── MainLayout.razor # Default page layout
│ └── ...
├── Pages/
│ ├── Index.razor # Home page
│ ├── Notes.razor # Feature page
│ └── ...
└── Shared/
├── NavMenu.razor # Navigation
└── ...
YourProject.App.Client
Blazor WebAssembly client - all client-side components.
Critical: The client module must have the [BluQubeRequester] attribute:
namespace YourProject.App.Client.Infrastructure;
[BluQubeRequester]
public static class Module
{
public static IServiceCollection AddYourProjectClientModule(
this IServiceCollection services)
{
// Register client services
services.AddScoped<NavigationService>();
// ...
return services;
}
}
File Naming Conventions
| Item | Convention | Example |
|---|---|---|
| Aggregate | <Name>Aggregate.cs | EquipmentAggregate.cs |
| Command | <Action><Aggregate>Command.cs | CreateEquipmentCommand.cs |
| Command Handler | <Action><Aggregate>CommandHandler.cs | CreateEquipmentCommandHandler.cs |
| Query | <Action><Aggregate>Query.cs | GetEquipmentByIdQuery.cs |
| Query Handler | <Action><Aggregate>QueryHandler.cs | GetEquipmentByIdQueryHandler.cs |
| Read Model | <Aggregate>Projection.cs | EquipmentProjection.cs |
| Domain Event | <Event>DomainEvent.cs | EquipmentCreatedDomainEvent.cs |
| Integration Event | <Event>IntegrationEvent.cs | EquipmentCreatedIntegrationEvent.cs |
| Events Handler | <Event>Handler.cs | EquipmentCreatedHandler.cs |
Project Files
peardrop.json
PearDrop CLI configuration and project manifest metadata:
{
"projectName": "YourProject",
"projects": [
{
"name": "YourProject.App",
"path": "source/YourProject.App",
"role": "app",
"moduleName": "Module",
"registrationMethod": "AddYourProjectAppModule"
},
{
"name": "YourProject.App.Client",
"path": "source/YourProject.App.Client",
"role": "client"
},
{
"name": "YourProject.Equipment",
"path": "source/modules/YourProject.Equipment",
"role": "module",
"moduleName": "Equipment",
"registrationMethod": "AddEquipmentModule"
},
{
"name": "YourProject.Equipment.Client",
"path": "source/modules/YourProject.Equipment.Client",
"role": "module-client"
}
],
"appProject": "source/YourProject.App",
"clientProject": "source/YourProject.App.Client",
"appRegistrationMethod": "AddYourProjectAppModule",
"frameworkVersion": "10.0.0",
"usedModules": [
"core",
"authentication",
"files",
"equipment"
],
"auth": {
"configured": true,
"mode": "hybrid",
"internalEnabled": true,
"externalProviders": ["entra"],
"externalProviderCount": 1
}
}
Schema notes:
projects[](preferred) - Structured array of all projects with metadata:name- Project name (e.g., "YourProject.App")path- Relative path from manifest rootrole- Project role:app,client,module, ormodule-clientmoduleName- Module identifier for usedModules trackingregistrationMethod- Service registration method name
appProject,clientProject- Legacy fields for backward compatibility (resolved fromprojects[]if missing)appRegistrationMethod- Optional, defaults toAdd{ProjectName}AppframeworkVersion- Used to resolve package versionsusedModules- Automatically maintained by CLI commands (add module,new module,add auth, etc.)auth- High-level summary (not runtime config); runtime settings remain inappsettings.json
CLI commands automatically update the manifest:
peardrop new moduleadds module entries toprojects[]andusedModulespeardrop add moduleadds framework modules tousedModulespeardrop add auth entra/remove auth entraupdatesauthsummarypeardrop extract-contextadds extracted module toprojects[]
nuget.config
NuGet feed configuration:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="peardrop-feed" value="https://your-nuget-feed/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
docker-compose.yml
Local service definitions for development:
version: '3.8'
services:
sql-server:
image: mcr.microsoft.com/mssql/server:latest
ports:
- "1440:1433"
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=YourSecurePassword123!
redis:
image: redis:latest
ports:
- "6379:6379"
Best Practices
- One aggregate per context folder - Keep related logic together
- Commands & Handlers together - Easy to find and maintain
- Read models separate - Optimize queries without touching domain
- Lazy initialization - Use
Lazy<T>in read models for performance - Specifications - Use query specifications to encapsulate filters
- No circular dependencies - Domain shouldn't reference queries
Using the PearDrop CLI
The CLI automatically scaffolds files in the correct structure:
# Create aggregate with properties
peardrop add aggregate Equipment --properties "Name:string,Category:string"
# Create command
peardrop add command CreateEquipment --aggregate Equipment --properties "Name:string"
# Create query
peardrop add query GetEquipmentById --aggregate Equipment --properties "EquipmentId:Guid"
# Create integration event
peardrop add integration-event EquipmentCreated --aggregate Equipment
See CLI Usage for more details.