You are an expert .NET engineer working on Foundatio.Repositories, a production-grade repository pattern library built on top of Foundatio. This library is used by developers to build robust data access layers with Elasticsearch. 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.
Foundatio.Repositories provides a generic repository pattern implementation built on Foundatio building blocks:
- Repository Pattern (
IRepository<T>,IReadOnlyRepository<T>) - CRUD operations with async events - Searchable Repositories (
ISearchableRepository<T>) - Dynamic querying with Foundatio.Parsers - Elasticsearch Implementation - Full-featured Elasticsearch repository with index management
- Caching Integration - Real-time cache invalidation with distributed cache support
- Message Bus Integration - Entity change notifications for real-time applications
- Patch Operations - JSON patch, partial document, and script-based updates
- Soft Deletes - Built-in soft delete support with query filtering
- Document Versioning - Optimistic concurrency with version tracking
- Index Management - Schema versioning, daily/monthly strategies, migrations
- Jobs - Index maintenance, snapshots, reindexing
Design principles: interface-first, built on Foundatio primitives, Elasticsearch-optimized, testable with in-memory implementations.
# Build
dotnet build Foundatio.Repositories.slnx
# Test shared repository tests
dotnet test tests/Foundatio.Repositories.Tests/Foundatio.Repositories.Tests.csproj
# Test Elasticsearch implementation (requires running Elasticsearch)
docker compose up -d
dotnet test tests/Foundatio.Repositories.Elasticsearch.Tests/Foundatio.Repositories.Elasticsearch.Tests.csproj
# Format code
dotnet format Foundatio.Repositories.slnxNote: When building within a workspace, use Foundatio.All.slnx instead to include all Foundatio projects in the build and test cycle.
src
├── Foundatio.Repositories # Core repository abstractions
│ ├── Exceptions # Repository-specific exceptions
│ ├── Extensions # Extension methods for repositories
│ ├── JsonPatch # JSON patch operation support
│ ├── Migration # Document migration infrastructure
│ ├── Models # Entity interfaces and base models
│ ├── Options # Command and query options
│ ├── Queries # Query builders and interfaces
│ └── Utility # ObjectId, helpers
└── Foundatio.Repositories.Elasticsearch # Elasticsearch implementation
├── Configuration # Index configuration and mappings
├── CustomFields # Dynamic field support
├── Extensions # Elasticsearch-specific extensions
├── Jobs # Maintenance and migration jobs
├── Options # Elasticsearch-specific options
├── Queries # Query builders for Elasticsearch
├── Repositories # Base repository implementations
└── Utility # Elasticsearch utilities
tests
├── Foundatio.Repositories.Tests # Core repository unit tests
└── Foundatio.Repositories.Elasticsearch.Tests # Elasticsearch integration tests
└── Repositories # Test repository implementations
├── Configuration # Test index configurations
├── Models # Test entity models
└── Queries # Test query implementations
samples
└── Foundatio.SampleApp # Sample Blazor application
├── Client # Blazor WebAssembly client
├── Server # ASP.NET Core server with repositories
└── Shared # Shared models
- Follow
.editorconfigrules and Microsoft C# conventions - Run
dotnet formatto auto-format code - Match existing file style; minimize diffs
- No code comments unless necessary—code should be self-explanatory
- Pattern matching over equality operators: Use
is null/is not nullinstead of== null/!= null. Useis 0/is 1instead of== 0/== 1 - Guard clauses: Use
ArgumentNullException.ThrowIfNull(param)instead of manualif (param == null) throw - Lambda bodies: Prefer multi-line bodies for lambda expressions—no single-line
{ action(); return true; }patterns - Raw string literals: Use
"""..."""for multi-line strings. Never use string concatenation (+) to build script or query strings - Domain-correct syntax in scripts: Verify operator syntax for the target language (e.g., Painless uses
==not===; NEST uses specific query DSL methods)
- Interface-first design: All core features expose interfaces (
IRepository<T>,ISearchableRepository<T>) - Dependency Injection: Use constructor injection; extend via
IServiceCollectionextensions - Foundatio Integration: Built on Foundatio primitives (caching, messaging, queues, jobs)
- Naming:
Foundatio.Repositories.[Feature]for projects,I[Feature]Repositoryfor interfaces - Elasticsearch Provider: Primary implementation in
Foundatio.Repositories.Elasticsearch
- Write complete, runnable code—no placeholders, TODOs, or
// existing code...comments - Use modern C# features: pattern matching, nullable references,
isexpressions, target-typednew() - Follow SOLID, DRY principles; remove unused code and parameters
The codebase has NRT enabled. Follow these patterns:
- Entity/model properties (
= null!): Acceptable for properties set by deserialization or Elasticsearch mapping. Preferrequiredkeyword where construction is controlled. - Expression tree
!: Required when expressions box nullable value types toobject?but the delegate expectsobject(e.g.,SortDescending,InferField). The value is never evaluated. .Where(x => x is not null).Select(x => x!): The compiler can't prove non-null through a lambda boundary after filtering. This is the idiomatic post-filter pattern.null!in testAssert.Throwscalls: Intentional contract violation to test null guards.CommandOptionsDescriptor<T>?: The descriptor parameter is nullable on all repository interfaces. Useoptions?.Configure()to convert toICommandOptions?when forwarding, or pass directly since implementations handle null.SafeGetOption<T>returns[MaybeNull]: Callers that omit the default value for reference types must null-check the result. Use?.or pattern matching before dereferencing.- Avoid
!to forward nullable parameters: Prefer making the parameter nullable on the interface, or using an overload that accepts nullable. - Clear, descriptive naming; prefer explicit over clever
- Use
AnyContext()(e.g.,ConfigureAwait(false)) in library code (not in tests) - Prefer
ValueTask<T>for hot paths that may complete synchronously - Always dispose resources: use
usingstatements orIAsyncDisposable - Handle cancellation tokens properly: check
token.IsCancellationRequested, pass through call chains
- Async suffix: All async methods end with
Async(e.g.,GetAsync,AddAsync) - CancellationToken: Last parameter, defaulted to
defaultin public APIs - Extension methods: Place in
Extensions/directory, use descriptive class names (e.g.,FindResultsExtensions) - Logging: Use structured logging with
ILogger, log at appropriate levels - Exceptions: Use
ArgumentException.ThrowIfNullOrEmpty(parameter)for validation. For repository-specific errors, useDocumentNotFoundException,DocumentValidationException,VersionConflictDocumentException. This ensures consumers get predictable exception types. ThrowArgumentNullException,ArgumentException,InvalidOperationExceptionwith clear messages for general validation and operation errors.
- 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
- 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
ValueTaskfor hot paths - Dispose resources promptly: Don't hold connections/handles longer than needed
- Gather context: Read related files, search for similar implementations, understand the full scope
- Research patterns: Find existing usages of the code you're modifying using grep/semantic search
- Understand completely: Know the problem, side effects, and edge cases before coding
- Plan the approach: Choose the simplest solution that satisfies all requirements
- Check dependencies: Verify you understand how changes affect dependent code
Before writing any implementation code, think critically:
- What could go wrong? Consider race conditions, null references, edge cases, resource exhaustion
- What are the failure modes? Network failures, timeouts, out-of-memory, concurrent access
- What assumptions am I making? Validate each assumption against the codebase
- Is this the root cause? Don't fix symptoms—trace to the core problem
- Will this scale? Consider performance under load, memory allocation patterns
- Is there existing code that does this? Search before creating new utilities
Always write or extend tests before implementing changes:
- Find existing tests first: Search for tests covering the code you're modifying
- Extend existing tests: Add test cases to existing test classes/methods when possible for maintainability
- Write failing tests: Create tests that demonstrate the bug or missing feature
- Implement the fix: Write minimal code to make tests pass
- Refactor: Clean up while keeping tests green
- 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.
- 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 buildafter each logical change to catch errors early - Test continuously: Run
dotnet testfrequently to verify correctness - Match style: Follow the patterns in surrounding code exactly
- Fix issues you find: If you discover a correctness issue—whether pre-existing or introduced by your changes—fix it. Don't dismiss problems as "out of scope" or "pre-existing." If the fix is trivial, just do it. If it's non-trivial, present the issue and a proposed plan to the user and ask whether to address it now.
Before marking work complete, verify:
- Builds successfully:
dotnet build Foundatio.Repositories.slnxexits with code 0 - All tests pass:
dotnet test Foundatio.Repositories.slnxshows no failures - No new warnings: Check build output for new compiler warnings
- API compatibility: Public API changes are intentional and backward-compatible when possible
- Documentation updated: XML doc comments added/updated for public APIs
- Interface documentation: Update interface definitions and docs with any API changes
- Feature documentation: Add entries to docs/ folder for new features or significant changes
- Skill updated: When changing core abstractions, query APIs, patch behavior, or docs, update
.agents/skills/foundatio-repositories/SKILL.mdto keep patterns, gotchas, and the interface hierarchy in sync. The skill delegates to context7 for full docs, so only compact patterns and non-obvious gotchas need maintenance. - Breaking changes flagged: Clearly identify any breaking changes for review
- 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
After the code compiles and tests pass, perform a design quality review before considering work complete. Tests passing means the code is correct—this review ensures it is also well-designed, complete, and maintainable. Walk through each check explicitly:
For every abstraction, type, and mechanism you introduced, ask:
- Is there a simpler way? If you used serialization, reflection, or complex infrastructure—could a simple callback, flag, or return value achieve the same result? Prefer the approach with the fewest moving parts.
- Who controls the behavior? Prefer giving callers explicit control (e.g.,
Func<T, bool>) over magic behavior (e.g., automatic JSON comparison). Callers know their domain better than infrastructure code. - Would a senior engineer question this? If the approach needs a paragraph to justify, it's probably too complex.
- Return types: Do similar methods return similar types? If one overload returns
Task<bool>, related overloads should follow the same pattern. - Parameter validation: Every public method entry point validates inputs the same way (
ArgumentNullException.ThrowIfNull, pattern matching). Check that refactoring didn't silently drop null checks or change validation behavior (e.g.,action?.Invokebecomingaction(...)without a null guard). - Naming and style: Use
ispattern matching over==for null/zero/type checks. UseThrowIfNullover manualif == nullthrows. Match the surrounding code exactly—don't introduce stylistic inconsistencies in your changes.
- No TODO comments for core functionality: If a feature has a TODO for a path that should work, implement it now. TODOs are acceptable for known future optimizations, not for incomplete behavior that callers expect to work.
- All code paths: If single-document path handles noop detection, the bulk path must too. If a method has early exits, verify they run before any side effects (e.g.,
EnsureIndexAsync). - Side effects gated correctly: Cache invalidation, change notifications, and message publishing should only fire when something actually changed. Trace every side-effect trigger and verify it's gated on the actual modification status.
- XML docs on public types: Every public class and interface method has XML docs that describe behavior, not just parameters. For operations with nuanced behavior (like noop detection varying by type), document the specifics in
<remarks>or<returns>tags. - Feature docs updated: Any new behavior, return type change, or API addition needs corresponding updates in
docs/guide/. - Code samples compile: Mentally (or actually) verify every code sample in documentation would compile and produce the described result. Use modern C# idioms (raw strings, not string concat; correct operator syntax for the target language like Painless
==not===). - Limitations documented: If a feature has known limitations (e.g., cached bulk path overcount), document them explicitly—both in code comments and in user-facing docs.
- Hot path allocations: Did you introduce new allocations (serialization, string building, LINQ
.ToList()) in paths that execute per-document or per-request? Can they be avoided? - Unnecessary I/O: Are early exits placed before any network calls? An empty operation should never trigger an Elasticsearch request.
- Bulk operation efficiency: For batch paths, verify you're not sending empty bulk requests or including unchanged documents in writes.
When changing types, signatures, or internal behavior:
- Trace all callers: Search for every usage of the changed API. Verify each caller still works correctly with the new semantics.
- Defensive code preserved: If the original code had null-safety (
?.Invoke), ensure the refactored version maintains equivalent safety or adds explicit validation. - Backward compatibility: If the public API signature changes, verify that existing calling code compiles and behaves identically without modification.
When PR feedback, audits, or your own review surfaces a correctness issue:
- Never dismiss as "pre-existing" or "out of scope": If it's a real correctness problem, it needs to be addressed regardless of who introduced it.
- Trivial fixes: Just fix them immediately—no need to ask.
- Non-trivial fixes: Present the issue, explain the impact, propose a fix plan with scope, and ask whether to address it now or track it separately.
- Don't rationalize away problems: "It works in practice" or "callers probably don't hit this" are not acceptable reasons to skip a fix. Correctness matters.
- 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
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
- xUnit as the primary testing framework
- ElasticRepositoryTestBase provides shared base class for Elasticsearch integration testing
- Follow Microsoft unit testing best practices
- Search for existing tests:
dotnet test --filter "FullyQualifiedName~MethodYouAreChanging" - Extend existing test classes: Add new
[Fact]or[Theory]cases to existing files - Write the failing test first: Verify it fails for the right reason
- Implement minimal code: Just enough to pass the test
- Add edge case tests: Null inputs, empty collections, boundary values, concurrent access
- Run full test suite: Ensure no regressions
- 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
Use the pattern: MethodName_StateUnderTest_ExpectedBehavior
Examples:
AddAsync_WithValidDocument_ReturnsDocumentWithIdGetByIdAsync_WhenNotFound_ReturnsNullRemoveAsync_WithSoftDelete_SetsIsDeletedFlag
Follow the AAA (Arrange-Act-Assert) pattern:
[Fact]
public async Task AddAsync_WithValidDocument_ReturnsDocumentWithId()
{
// Arrange
var identity = IdentityGenerator.Generate();
// Act
var result = await _identityRepository.AddAsync(identity);
// Assert
Assert.NotNull(result?.Id);
}Use [Theory] with [InlineData] for multiple scenarios:
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public async Task GetByIdAsync_WithInvalidId_ReturnsNull(string id)
{
var result = await _identityRepository.GetByIdAsync(id);
Assert.Null(result);
}- Mirror the main code structure (e.g.,
Repositories/tests for repository implementations) - Use constructors and
IDisposablefor setup/teardown - Inject
ITestOutputHelperfor test logging - Inherit from
ElasticRepositoryTestBasefor Elasticsearch integration tests
- Use in-memory implementations (from Foundatio) for unit tests
- For Elasticsearch tests, use
docker compose upto start Elasticsearch - Inherit from
ElasticRepositoryTestBasewhich provides_configuration,_cache,_client - Use
RemoveDataAsync()to clean state between tests - Keep integration tests separate from unit tests
# All tests
dotnet test Foundatio.Repositories.slnx
# Core repository tests only
dotnet test tests/Foundatio.Repositories.Tests/Foundatio.Repositories.Tests.csproj
# Elasticsearch tests (requires running Elasticsearch)
dotnet test tests/Foundatio.Repositories.Elasticsearch.Tests/Foundatio.Repositories.Elasticsearch.Tests.csproj
# Specific test file
dotnet test --filter "FullyQualifiedName~RepositoryTests"
# With logging
dotnet test --logger "console;verbosity=detailed"- Reproduce with minimal steps
- Understand the root cause before fixing
- Test the fix thoroughly
- Document non-obvious fixes in code if needed
- 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
- README.md - Overview and feature list
- .agents/skills/foundatio-repositories/ - Agent skill for consumers (uses context7 for live docs)
- samples/ - Sample Blazor application with repository usage
- Foundatio - Core building blocks this library depends on
- Foundatio.Parsers - Query parsing for searchable repositories