Common Patterns
Frequently used patterns in PearDrop applications.
Quick Reference
For a complete end-to-end example, see Building a Complete CRUD Feature.
Quick Links
Core Patterns
- Building a Complete CRUD Feature - Full implementation walkthrough
- Form Validation - Server and client validation
- Search & Filtering - Queries with filters and pagination
Advanced Patterns
- Security & Authorization - Resource-based authorization
- Domain Events - Transactional domain coordination
- Integration Events with CAP - Async processing
- BFF Platform Modules - Split frontend orchestration into focused modules
- Testing Guide - Unit and integration tests
Authentication & Multi-Tenancy
- Authentication Overview - Auth setup and patterns
- External Authentication - Entra ID, OAuth providers
- MFA Configuration - Multi-factor authentication
- Multi-Tenant Examples - Tenant isolation patterns
Pattern Categories
Data Access Patterns
// Write: Command → Handler → Aggregate → Repository → DbContext
var command = new CreateNoteCommand(title, content);
var result = await commandRunner.Send(command);
// Read: Query → Handler → Read Models → DbSet (no tracking)
var query = new GetMyNotesQuery();
var result = await queryRunner.Send(query);
Authorization Patterns
// Command authorizer (before handler)
public class UpdateNoteCommandAuthorizer
: IAuthorizationHandler<UpdateNoteCommand, UpdateNoteCommandResult>
{
public async Task<AuthorizationResult> AuthorizeAsync(
UpdateNoteCommand command,
CancellationToken cancellationToken)
{
// Check: User can only update their own notes
var note = await repository.FindOne(new ByIdSpec(command.NoteId));
if (note.UserId != currentUserId)
{
return AuthorizationResult.Fail("Cannot edit another user's note");
}
return AuthorizationResult.Succeed();
}
}
Validation Patterns
// FluentValidation (server-side)
public class CreateNoteCommandValidator : AbstractValidator<CreateNoteCommand>
{
public CreateNoteCommandValidator()
{
RuleFor(x => x.Title)
.NotEmpty().WithMessage("Title is required")
.MaximumLength(200);
RuleFor(x => x.Content)
.NotEmpty()
.MaximumLength(5000);
}
}
Repository Patterns
// Load aggregate
var noteMaybe = await repository.FindOne(
new ByIdSpecification<NoteAggregate>(noteId),
cancellationToken);
if (noteMaybe.HasNoValue)
{
return CommandResult.Failed(new BluQubeErrorData(
ErrorCodes.NotFound, "Note not found"));
}
var note = noteMaybe.Value;
// Execute business logic
note.Archive();
// Save changes
await repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
Query Patterns
// Simple query
var notes = await readModels.Notes
.Where(n => n.UserId == currentUserId && !n.IsArchived)
.OrderByDescending(n => n.UpdatedAt)
.ToListAsync(cancellationToken);
// Filtered query with pagination
var results = await readModels.Products
.Where(p => p.CategoryId == categoryId)
.Where(p => p.Price >= minPrice && p.Price <= maxPrice)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
Blazor Component Patterns
@* List with edit capability *@
<div class="items-list">
@foreach (var item in items)
{
<div class="item-card" @onclick="() => EditItem(item.Id)">
<h3>@item.Title</h3>
<p>@item.Description</p>
</div>
}
</div>
@* Form with validation *@
<EditForm Model="model" OnValidSubmit="Save">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText @bind-Value="model.Title" />
<ValidationMessage For="@(() => model.Title)" />
<button type="submit" disabled="@isSaving">Save</button>
</EditForm>
Next Steps
Choose a recipe that matches your current needs:
- Building a new feature? → Complete CRUD Feature
- Need validation? → Form Validation Patterns
- Adding search? → Search and Filtering
- Securing features? → Security & Authorization
See the full cookbook for detailed recipes on each pattern.