Skip to main content

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

ItemConventionExample
Aggregate<Name>Aggregate.csEquipmentAggregate.cs
Command<Action><Aggregate>Command.csCreateEquipmentCommand.cs
Command Handler<Action><Aggregate>CommandHandler.csCreateEquipmentCommandHandler.cs
Query<Action><Aggregate>Query.csGetEquipmentByIdQuery.cs
Query Handler<Action><Aggregate>QueryHandler.csGetEquipmentByIdQueryHandler.cs
Read Model<Aggregate>Projection.csEquipmentProjection.cs
Domain Event<Event>DomainEvent.csEquipmentCreatedDomainEvent.cs
Integration Event<Event>IntegrationEvent.csEquipmentCreatedIntegrationEvent.cs
Events Handler<Event>Handler.csEquipmentCreatedHandler.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 root
    • role - Project role: app, client, module, or module-client
    • moduleName - Module identifier for usedModules tracking
    • registrationMethod - Service registration method name
  • appProject, clientProject - Legacy fields for backward compatibility (resolved from projects[] if missing)
  • appRegistrationMethod - Optional, defaults to Add{ProjectName}App
  • frameworkVersion - Used to resolve package versions
  • usedModules - Automatically maintained by CLI commands (add module, new module, add auth, etc.)
  • auth - High-level summary (not runtime config); runtime settings remain in appsettings.json
Automatic Manifest Updates

CLI commands automatically update the manifest:

  • peardrop new module adds module entries to projects[] and usedModules
  • peardrop add module adds framework modules to usedModules
  • peardrop add auth entra / remove auth entra updates auth summary
  • peardrop extract-context adds extracted module to projects[]

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

  1. One aggregate per context folder - Keep related logic together
  2. Commands & Handlers together - Easy to find and maintain
  3. Read models separate - Optimize queries without touching domain
  4. Lazy initialization - Use Lazy<T> in read models for performance
  5. Specifications - Use query specifications to encapsulate filters
  6. 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.