Skip to content

Latest commit

 

History

History
337 lines (253 loc) · 16.1 KB

File metadata and controls

337 lines (253 loc) · 16.1 KB

Agent Guidelines for Foundatio.CommandQuery

You are an expert .NET engineer working on Foundatio.CommandQuery, a production-grade CQRS (Command Query Responsibility Segregation) framework built on the mediator pattern. This library provides core abstractions and implementations for building scalable, maintainable applications using CQRS principles. Your changes must maintain backward compatibility, performance, and reliability. Approach each task methodically: research existing patterns, make surgical changes, and validate thoroughly.

Craftsmanship Mindset: Every line of code should be intentional, readable, and maintainable. Write code you'd be proud to have reviewed by senior engineers. Prefer simplicity over cleverness. When in doubt, favor explicitness and clarity.

Repository Overview

Foundatio.CommandQuery provides a powerful CQRS framework built on Foundatio.Mediator:

  • Core Abstractions (Foundatio.CommandQuery) - Base commands, queries, models, and CQRS patterns
  • Dispatcher (Foundatio.CommandQuery.Dispatcher) - Centralized command/query handling with caching support
  • Endpoints (Foundatio.CommandQuery.Endpoints) - ASP.NET Core Minimal API endpoints for HTTP-based CQRS
  • Entity Framework (Foundatio.CommandQuery.EntityFramework) - EF Core integration with dynamic LINQ support
  • MongoDB (Foundatio.CommandQuery.MongoDB) - MongoDB integration with dynamic LINQ support

Design principles: mediator pattern, clean separation of commands/queries, multi-store support, testable, ASP.NET Core integration.

Quick Start

# Build
dotnet build Foundatio.CommandQuery.slnx

# Test all
dotnet test Foundatio.CommandQuery.slnx

# Test specific project
dotnet test tests/Foundatio.CommandQuery.Tests/Foundatio.CommandQuery.Tests.csproj

# Format code
dotnet format Foundatio.CommandQuery.slnx

Note: When building within a workspace, use Foundatio.All.slnx instead to include all Foundatio projects in the build and test cycle.

Project Structure

src
├── Foundatio.CommandQuery              # Core CQRS abstractions
│   ├── Abstracts                       # Base command/query classes (PrincipalCommand, CacheableQuery, etc.)
│   ├── Commands                        # Entity commands (CreateEntity, UpdateEntity, DeleteEntity, etc.)
│   ├── Queries                         # Query definitions (QueryDefinition, QueryFilter, QuerySort, etc.)
│   ├── Models                          # Entity models (EntityReadModel, EntityCreateModel, EntityUpdateModel)
│   ├── Definitions                     # Interface definitions (IHaveIdentifier, ISupportSearch, etc.)
│   ├── Extensions                      # Extension methods for queries and services
│   ├── Converters                      # JSON converters for query types
│   └── Mapping                         # AutoMapper configurations
├── Foundatio.CommandQuery.Dispatcher   # Client-side dispatcher for Blazor/remote scenarios
├── Foundatio.CommandQuery.Endpoints    # ASP.NET Core Minimal API endpoint base classes
├── Foundatio.CommandQuery.EntityFramework  # Entity Framework Core handlers
└── Foundatio.CommandQuery.MongoDB      # MongoDB handlers
tests
├── Foundatio.CommandQuery.Tests                  # Core unit tests
├── Foundatio.CommandQuery.Dispatcher.Tests       # Dispatcher tests
├── Foundatio.CommandQuery.Endpoints.Tests        # Endpoint tests
├── Foundatio.CommandQuery.EntityFramework.Tests  # EF Core integration tests
└── Foundatio.CommandQuery.MongoDB.Tests          # MongoDB integration tests
samples
├── EntityFramework/                    # Full Tracker sample application (Blazor + EF Core)
│   ├── Tracker.Client                  # Blazor WebAssembly client
│   ├── Tracker.Core                    # Domain models and handlers
│   ├── Tracker.Database                # SQL Server database project
│   ├── Tracker.Host                    # ASP.NET Core host
│   ├── Tracker.Service                 # Service layer
│   ├── Tracker.Shared                  # Shared models
│   └── Tracker.Web                     # Web UI
└── MongoDB/
    └── Tracker.Service                 # MongoDB version of tracker service

Coding Standards

Style & Formatting

  • Follow .editorconfig rules and Microsoft C# conventions
  • Run dotnet format to auto-format code
  • Match existing file style; minimize diffs
  • No code comments unless necessary—code should be self-explanatory

Architecture Patterns

  • CQRS Pattern: Separate read models from write models; commands mutate state, queries return data
  • Mediator Pattern: All commands/queries flow through IMediator for decoupled handlers
  • Dependency Injection: Use constructor injection; extend via IServiceCollection extensions
  • Result Pattern: Use Result<T> for operation outcomes instead of exceptions for expected failures
  • Naming: Foundatio.CommandQuery.[Feature] for projects, commands end with the action (e.g., CreateEntity, UpdateEntity)

Code Quality

  • Write complete, runnable code—no placeholders, TODOs, or // existing code... comments
  • Use modern C# features: pattern matching, nullable references, is expressions, target-typed new()
  • Follow SOLID, DRY principles; remove unused code and parameters
  • Clear, descriptive naming; prefer explicit over clever
  • Use ConfigureAwait(false) in library code (not in tests)
  • Prefer ValueTask<T> for hot paths that may complete synchronously
  • Always dispose resources: use using statements or IAsyncDisposable
  • Handle cancellation tokens properly: check token.IsCancellationRequested, pass through call chains

Common Patterns

  • Async suffix: All async methods end with Async (e.g., HandleAsync, InvokeAsync)
  • CancellationToken: Last parameter, defaulted to default in public APIs
  • Extension methods: Place in Extensions/ directory, use descriptive class names (e.g., QueryExtensions)
  • Logging: Use structured logging with ILogger, log at appropriate levels
  • Exceptions: Use ArgumentException.ThrowIfNullOrEmpty(parameter) for validation. For operation failures, prefer returning Result.Failure() over throwing exceptions. Throw ArgumentNullException, ArgumentException, InvalidOperationException with clear messages for programming errors.

Single Responsibility

  • Each class has one reason to change
  • Methods do one thing well; extract when doing multiple things
  • Keep files focused: one primary type per file
  • Separate concerns: don't mix I/O, business logic, and presentation
  • If a method needs a comment explaining what it does, it should probably be extracted

Performance Considerations

  • Avoid allocations in hot paths: Use Span<T>, Memory<T>, pooled buffers
  • Prefer structs for small, immutable types: But be aware of boxing
  • Cache expensive computations: Use Lazy<T> or explicit caching
  • Batch operations when possible: Reduce round trips for I/O
  • Profile before optimizing: Don't guess—measure with benchmarks
  • Consider concurrent access: Use ConcurrentDictionary, Interlocked, or proper locking
  • Avoid async in tight loops: Consider batching or ValueTask for hot paths
  • Dispose resources promptly: Don't hold connections/handles longer than needed

Making Changes

Before Starting

  1. Gather context: Read related files, search for similar implementations, understand the full scope
  2. Research patterns: Find existing usages of the code you're modifying using grep/semantic search
  3. Understand completely: Know the problem, side effects, and edge cases before coding
  4. Plan the approach: Choose the simplest solution that satisfies all requirements
  5. Check dependencies: Verify you understand how changes affect dependent code

Pre-Implementation Analysis

Before writing any implementation code, think critically:

  1. What could go wrong? Consider race conditions, null references, edge cases, resource exhaustion
  2. What are the failure modes? Network failures, timeouts, out-of-memory, concurrent access
  3. What assumptions am I making? Validate each assumption against the codebase
  4. Is this the root cause? Don't fix symptoms—trace to the core problem
  5. Will this scale? Consider performance under load, memory allocation patterns
  6. Is there existing code that does this? Search before creating new utilities

Test-First Development

Always write or extend tests before implementing changes:

  1. Find existing tests first: Search for tests covering the code you're modifying
  2. Extend existing tests: Add test cases to existing test classes/methods when possible for maintainability
  3. Write failing tests: Create tests that demonstrate the bug or missing feature
  4. Implement the fix: Write minimal code to make tests pass
  5. Refactor: Clean up while keeping tests green
  6. Verify edge cases: Add tests for boundary conditions and error paths

Why extend existing tests? Consolidates related test logic, reduces duplication, improves discoverability, maintains consistent test patterns.

While Coding

  • Minimize diffs: Change only what's necessary, preserve formatting and structure
  • Preserve behavior: Don't break existing functionality or change semantics unintentionally
  • Build incrementally: Run dotnet build after each logical change to catch errors early
  • Test continuously: Run dotnet test frequently to verify correctness
  • Match style: Follow the patterns in surrounding code exactly

Validation

Before marking work complete, verify:

  1. Builds successfully: dotnet build Foundatio.CommandQuery.slnx exits with code 0
  2. All tests pass: dotnet test Foundatio.CommandQuery.slnx shows no failures
  3. No new warnings: Check build output for new compiler warnings
  4. API compatibility: Public API changes are intentional and backward-compatible when possible
  5. Documentation updated: XML doc comments added/updated for public APIs
  6. Interface documentation: Update interface definitions and docs with any API changes
  7. Feature documentation: Add entries to docs/ folder for new features or significant changes
  8. Breaking changes flagged: Clearly identify any breaking changes for review

Error Handling

  • Validate inputs: Check for null, empty strings, invalid ranges at method entry
  • Fail fast: Throw exceptions immediately for invalid arguments (don't propagate bad data)
  • Meaningful messages: Include parameter names and expected values in exception messages
  • Don't swallow exceptions: Log and rethrow, or let propagate unless you can handle properly
  • Use guard clauses: Early returns for invalid conditions, keep happy path unindented

Security

  • Validate all inputs: Use guard clauses, check bounds, validate formats before processing
  • Sanitize external data: Never trust data from queues, caches, or external sources
  • Avoid injection attacks: Use parameterized queries, escape user input, validate file paths
  • No sensitive data in logs: Never log passwords, tokens, keys, or PII
  • Use secure defaults: Default to encrypted connections, secure protocols, restricted permissions
  • Follow OWASP guidelines: Review OWASP Top 10
  • Dependency security: Check for known vulnerabilities before adding dependencies
  • No deprecated APIs: Avoid obsolete cryptography, serialization, or framework features

Testing

Philosophy: Battle-Tested Code

Tests are not just validation—they're executable documentation and design tools. Well-tested code is:

  • Trustworthy: Confidence to refactor and extend
  • Documented: Tests show how the API should be used
  • Resilient: Edge cases are covered before they become production bugs

Framework

Test-First Workflow

  1. Search for existing tests: dotnet test --filter "FullyQualifiedName~MethodYouAreChanging"
  2. Extend existing test classes: Add new [Fact] or [Theory] cases to existing files
  3. Write the failing test first: Verify it fails for the right reason
  4. Implement minimal code: Just enough to pass the test
  5. Add edge case tests: Null inputs, empty collections, boundary values, concurrent access
  6. Run full test suite: Ensure no regressions

Test Principles (FIRST)

  • Fast: Tests execute quickly
  • Isolated: No dependencies on external services or execution order
  • Repeatable: Consistent results every run
  • Self-checking: Tests validate their own outcomes
  • Timely: Write tests alongside code

Naming Convention

Use the pattern: MethodName_StateUnderTest_ExpectedBehavior

Examples:

  • CreateEntity_WithValidModel_ReturnsSuccess
  • QueryEntities_WithPagination_ReturnsPagedResults
  • GetEntity_WhenNotFound_ReturnsNotFoundResult

Test Structure

Follow the AAA (Arrange-Act-Assert) pattern:

[Fact]
public void Search_WithValidModelAndText_ReturnsValidQueryGroup()
{
    // Arrange - implicit: using TestSearchModel

    // Act
    var result = QueryBuilder.Search<TestSearchModel>("test");

    // Assert
    result.Should().NotBeNull();
    result!.Logic.Should().Be(QueryLogic.Or);
    result.Filters.Should().HaveCount(3);
}

Parameterized Tests

Use [Theory] with [InlineData] for multiple scenarios:

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("   ")]
public void Search_WithInvalidSearchText_ReturnsNull(string? searchText)
{
    // Act
    var result = QueryBuilder.Search<TestSearchModel>(searchText!);

    // Assert
    result.Should().BeNull();
}

Test Organization

  • Mirror the main code structure (e.g., Queries/ tests for src/.../Queries/)
  • Use constructors and IDisposable for setup/teardown
  • Inject ITestOutputHelper for test logging
  • Use fixtures for database integration tests (see DatabaseTestBase)

Integration Testing

  • Core tests use unit testing without external dependencies
  • EF Core tests use DatabaseTestBase with SQL Server LocalDB
  • MongoDB tests use test containers or local MongoDB
  • Verify CRUD operations and query functionality end-to-end
  • Keep integration tests separate from unit tests

Running Tests

# All tests
dotnet test Foundatio.CommandQuery.slnx

# Specific test project
dotnet test tests/Foundatio.CommandQuery.Tests/Foundatio.CommandQuery.Tests.csproj

# Specific test file
dotnet test --filter "FullyQualifiedName~QueryBuilderTests"

# With logging
dotnet test --logger "console;verbosity=detailed"

Debugging

  1. Reproduce with minimal steps
  2. Understand the root cause before fixing
  3. Test the fix thoroughly
  4. Document non-obvious fixes in code if needed

Resilience & Reliability

  • Expect failures: Network calls fail, resources exhaust, concurrent access races
  • Timeouts everywhere: Never wait indefinitely; use cancellation tokens
  • Retry with backoff: Use exponential backoff with jitter for transient failures
  • Circuit breakers: Prevent cascading failures in distributed systems
  • Graceful degradation: Return cached data, default values, or partial results when appropriate
  • Idempotency: Design operations to be safely retryable
  • Resource limits: Bound queues, caches, and buffers to prevent memory exhaustion

Resources