Best Practices Quick Reference
Recommended patterns for building reliable PearDrop applications. For detailed guidance, see the referenced pages below.
Quick Links to Detailed Guides
| Topic | Location | Coverage |
|---|---|---|
| Testing | Testing PearDrop Applications | Unit tests, integration tests, test strategies |
| Repositories | Repositories & Specifications | Repository pattern, specifications, best practices |
| Security | Security & Authorization | Authorization, input validation, data protection |
| Monitoring | Operations & Monitoring | Logging, performance monitoring, health checks |
| CQRS Operations | CQRS Operations | Command/query execution patterns |
| Form Validation | Form Validation Patterns | Blazor validation integration |
Key Patterns Summary
✅ Command Design (Essential)
// Required parameters in constructor
public sealed record CreateProductCommand(
string Name,
decimal Price) : ICommand;
References: CQRS Pattern, Commands with Results
✅ Aggregate Design (Essential)
// Private setters, domain methods encapsulate rules
public sealed class Product : Entity, IAggregateRoot
{
private Product() { } // EF Core
public string Name { get; private set; }
public ResultMonad Activate() { /* business logic */ }
}
References: Aggregates, Bounded Contexts
✅ Query Design (Essential)
// Read-only, use projections, filter at database
var products = await readModels.Products
.Where(p => p.CategoryId == categoryId)
.Select(p => new ProductDto(p.Id, p.Name, p.Price))
.Take(50)
.ToListAsync();
References: Read Models, Search and Filtering
✅ Repository Pattern (Essential)
// Check maybe value before accessing
var productMaybe = await repository.FindOne(
new ByIdSpecification<ProductAggregate>(id),
ct);
if (productMaybe.HasNoValue)
return CommandResult.Failed(ErrorCodes.NotFound);
var product = productMaybe.Value;
product.UpdatePrice(newPrice);
await repository.UnitOfWork.SaveEntitiesAsync(ct);
References: Repositories & Specifications
✅ Validation (Essential)
// Server-side validation with FluentValidation
public sealed class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
public CreateProductCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.MaximumLength(100);
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Price must be positive");
}
}
References: Command Validators & Authorizers, Form Validation Patterns
✅ Authorization (Important)
// Check permissions before operating
if (!authResult.Succeeded)
return CommandResult.Failed("Unauthorized");
// Validate in aggregate
if (note.OwnerId != userId)
return ResultMonad.Failure("Cannot modify note you don't own");
References: Security & Authorization, Command Validators & Authorizers
Essential vs. Advanced Patterns
| Pattern | When Needed | Learn More |
|---|---|---|
| Commands & Queries | Always | CQRS Pattern |
| Aggregates | Always | Aggregates |
| Read Models | For query performance | Read Models |
| Repositories | Always (for data access) | Repositories & Specifications |
| Validation | Always | Validators |
| Authorization | For security | Security & Authorization |
| Domain Events | For coordinating aggregates | Domain Events |
| Tests | Always | Testing Guide |
| Logging | For production | Monitoring Guide |
Common Mistakes
❌ Mistake: Public Setters on Aggregates
// BAD - Anyone can change state
public class Order
{
public OrderStatus Status { get; set; } // Public setter!
}
// GOOD - Only methods can change state
public sealed class Order
{
public OrderStatus Status { get; private set; } // Private setter
public ResultMonad Ship()
{
// Validates business rules before changing state
if (this.Status != OrderStatus.Paid)
return ResultMonad.Failure("Cannot ship unpaid order");
this.Status = OrderStatus.Shipped;
return ResultMonad.Success();
}
}
❌ Mistake: Multiple SaveEntitiesAsync Calls
// BAD - Two database transactions
await repository.UnitOfWork.SaveEntitiesAsync(ct);
aggregate.UpdateProperty(newValue);
await repository.UnitOfWork.SaveEntitiesAsync(ct); // Wrong!
// GOOD - Single transaction
aggregate.UpdateProperty(newValue);
await repository.UnitOfWork.SaveEntitiesAsync(ct); // One save
❌ Mistake: Querying Without Pagination
// BAD - Could load millions of rows
var allProducts = await readModels.Products.ToListAsync();
// GOOD - Paginated
var products = await readModels.Products
.Take(50)
.ToListAsync();
❌ Mistake: Trusting Client-Side Validation Only
// BAD - Browser can be bypassed
if (clientSideValidation.IsValid)
await SendToServer(command);
// GOOD - Validate on server
var result = await handler.Handle(command); // Handler has validator
if (!result.IsSuccess)
return BadRequest(result.Error);
Next Steps
Explore detailed guides for specific patterns:
- Complete CRUD Feature - See all patterns working together
- Testing PearDrop Applications - Comprehensive testing strategies
- Security & Authorization - Secure your application
- Monitoring & Operations - Production readiness
- Repositories & Specifications - Data access patterns
Remember: These are guidelines, not absolute rules. Adapt to your team's needs and project requirements.