You are an AI coding assistant for this repository.
- These instructions apply repository-wide unless a more specific
AGENTS.mdexists below the target path. - Follow
.editorconfigand nearby code before introducing a new style or abstraction. - Keep changes focused, maintainable, and production-ready. Preserve unrelated user changes.
- Use the SDK selected by
global.json; do not change the SDK or target-framework policy incidentally. - Run repository scripts and benchmarks on .NET 10. Target
net10.0in script entry points, benchmark projects, and generated evaluation projects; selecting the .NET 10 SDK alone does not select the .NET 10 runtime. Record the actual runtime in evaluation results, and compare configurations on the same runtime. - Implement necessary repository automation and evaluation scripts as file-based C# applications, run with
dotnet run --file path/to/script.cs -- ...using the repository SDK. Do not introduce Python scripts or require a separate scripting runtime. Name these files with descriptive PascalCase names, such asEvaluateScheduling.cs. Keep script dependencies minimal and document their arguments and validation. - Keep script entry points, implementations, and shared helpers under
scripts/; place reusable script utilities inscripts/Common. Do not put script implementation or helper code in benchmark, test, or production projects. Scripts may reference the production API they exercise; benchmark workloads belong in the benchmark project. - Find script entry points and usage guidance in the script index. Update the index when adding, renaming, or changing script entry points.
- Keep simple GitHub Actions release-tag parsing and package selection inline in the workflow YAML; do not add a standalone script solely for those steps.
- Keep the public API small and centered on single-request and explicit batch submission through
IRequestBatcher<TRequest>. - BufferQueue is an internal scheduling dependency. Do not expose its topics, partitions, producers, or consumers from RequestBatcher public APIs.
- Keep queue adapters and pending-request state under
src/RequestBatcher/PendingRequests, and dispatch undersrc/RequestBatcher/Scheduling. The coordinator owns public submission, lifecycle composition, and supervision of its single dispatch task. Keep internal handlers underHandlers, with delegate binding and adapters inHandlers/Delegate. Split diagnostics intoDiagnostics/Tracing,Diagnostics/Metrics, andDiagnostics/Logging; shared diagnostic metadata stays inDiagnostics. - Compose RequestBatcher through
IServiceCollection.AddRequestBatcher; callers must not need to register or configure BufferQueue. Production code registers BufferQueue into the application's container and must not build or own a nested service provider. - Avoid forwarding dependencies through coordinator or adapter properties. Pass shared dependencies directly to the component that owns the behavior; retain a forwarding property only when a clear lifecycle or abstraction boundary requires it.
- Register the handler in the same
AddRequestBatchercall and require an explicitServiceLifetime. Resolve scoped and transient handlers inside an async scope owned by one batch; never capture them in the singleton coordinator. - A request accepted by the coordinator must complete exactly once with success, cancellation, or the handler failure for its batch. Capacity must be released exactly once on every terminal path.
- Each response entry has one writer in its owning Handler batch. Await response-producing work before the Handler returns; completion boundaries publish response values to framework validation and caller result assembly. Keep per-entry storage simple, retain sequential duplicate checks, and validate missing responses before publishing success.
- RequestBatcher provides no global, partition-local, or partition-key handler execution ordering guarantee.
UsePartitionKeyroutes equal keys to one queue partition only; separate batches for equal keys can execute concurrently. Do not imply that an entire key is collected into one batch or that routing serializes execution. - Caller cancellation may remove work only before handler dispatch. Once a batch is dispatched, report its actual outcome so retrying callers cannot unknowingly duplicate side effects.
DisposeAsyncstops admission before draining accepted work. Keep lifecycle transitions idempotent and safe under concurrent calls.- Keep one top-level type per C# file, except for nested implementation details and test helpers. Namespaces mirror the owning project and source directory.
- Target the frameworks declared by project files and use only C# 12 language features.
- Keep nullable annotations, cancellation propagation, and
ConfigureAwait(false)usage correct and consistent. - Define observability tag keys and fixed tag values as internal constants. Keep Span tags in
RequestBatcherTraceTagsand Metrics tags inRequestBatcherMetricTags; maintain these separately even when their text matches. Keep instrument names inRequestBatcherMetricNamesand reuse them in recording and OpenTelemetry view configuration. Avoid scattered literals in production code; contract tests may use explicit strings to verify externally visible names. - Do not add per-file copyright or license headers. Repository licensing is defined by the root
LICENSEfile and package metadata. - Write code comments and XML documentation in English. Document every public API type and member; do not suppress
CS1591project-wide. - Use multiline XML documentation summaries: put
/// <summary>, the description, and/// </summary>on separate lines. Do not collapse a summary onto one line, including for enum members, properties, and benchmark methods. Keep longer explanations inremarksand useparamandreturnsfor their corresponding contracts. - Prefer concise modern C# when it clarifies the code. Use primary constructors when a type only captures dependencies or state and doing so does not broaden constructor accessibility. Retain explicit constructors when validation, defensive copies, resource ownership, cleanup, or non-public constructor visibility make the lifecycle clearer.
- Prefer base libraries and existing dependencies. Explain any new production dependency and compatibility impact.
- Do not add a repository
NuGet.configor hard-code a package source. - Do not decompile NuGet package assemblies. When implementation details of a dependency are necessary, inspect the corresponding source repository at the commit recorded in the package metadata.
- RequestBatcher is performance-sensitive. Review per-request and per-batch CPU, allocation, and synchronization costs in admission, queue adapters, dispatch, handler adapters, response completion, cancellation bookkeeping, and diagnostics.
- In these hot paths, use indexed
forloops for arrays, spans, and collections with O(1) indexed access, such asList<T>and the queue'sIReadOnlyList<T>snapshots. Do not useforeachfor these indexable collections. Enumeration through interfaces such asIEnumerable<T>orIReadOnlyList<T>can allocate or box an enumerator. - For a collection that remains unchanged during traversal, read
Countonce before the loop and reuse that value. Helpers scanning the same stable batch snapshot should share the captured count. Preserve live-count behavior when mutation during traversal is part of the contract. - Review interface
Countand indexer calls as well as enumerator costs. Replacingforeachwithforcan exchange enumeration overhead for repeated interface property calls; do not assume the JIT removes those calls. - Direct
foreachover arrays and spans can be lowered to indexed loops; concrete collections with value-type enumerators can also avoid enumerator allocation. This does not make traversal free or prove that either syntax is faster. Preserve native enumeration for non-indexable collections and asynchronous streams when required. Do not introduceToArray(),ToList(), repeatedElementAt()calls, or unsafe access merely to replace a loop with indexed access; preserve streaming, cancellation, and backpressure semantics. - Check the collection's static type and iteration behavior when reviewing a hot-path loop. Verify performance changes with the comparable benchmarks and correctness stress tests required below; a syntax change alone does not establish a performance gain. Compare execution time and allocation separately, and investigate or revise a loop rewrite that regresses representative workloads. Startup, service registration, tests, and samples do not need the same loop-style restriction.
- When maintaining a single language version of a document, use English. Generate a separate
zh-CNChinese version when needed, using a.zh-CN.mdfilename suffix (for example,README.zh-CN.md). - For a feature, public API, behavior, or architectural change, create or update the relevant design document before editing production code. The design must state the intended contract, internal flow, compatibility impact, and test coverage; implementation must follow that documented design.
- Design documents describe only the current intended design. Do not retain implementation history, rejected alternatives, superseded behavior, or APIs that no longer exist.
- Update the README when behavior, configuration, public APIs, lifecycle, or user-facing functionality changes.
- Keep
src/RequestBatcher/README.md, the NuGet package README, synchronized with relevant user-facing behavior, configuration, and usage guidance in the root README. - Keep usage examples compilable and focused on the public abstraction rather than internal queue mechanics.
- Chinese documents must reference Chinese diagrams, including text inside SVGs; do not reuse English diagrams when maintaining a Chinese version. Keep a matching PNG backup for each diagram and synchronize its language and content whenever the SVG changes. Prefer SVGs in Markdown and retain PNGs for publishing workflows that require raster images.
- Reference repository images with local relative paths in repository documentation. Only the NuGet package README
(
src/RequestBatcher/README.md) should use GitHub-hosted image URLs so images render on NuGet. - Document the durability boundary explicitly: RequestBatcher is in-process and does not recover pending requests after a process failure.
- For performance work, see the benchmark index and evaluation index. Preserve reusable validation scripts, benchmark workloads, concise evaluation conclusions, charts, and the documentation links that support them. Summaries must identify the measured source and runtime, relevant workload, correctness outcome, and limitations; local measurements do not establish universal performance guarantees.
- Keep bulk raw performance data and disposable outputs outside the repository: per-run CSV/JSON exports, benchmark logs, traces, failed trials, duplicate exports, scratch projects, and one-off investigation outputs. Do not commit or upload these artifacts unless explicitly requested. Keep only concise reviewed conclusions and reproduction guidance for experiments; do not retain obsolete dependency comparisons or one-time optimization and validation reports. Artifact cleanup does not authorize deleting existing README content, linked evaluation summaries, charts, or reusable validation code.
- Before committing changes to critical paths, run the relevant benchmarks and correctness stress tests listed in the benchmark index. Critical paths include single/batch admission, queue adapters, batching and dispatch, capacity and concurrency control, response completion, cancellation and draining, tracing/metrics hot paths, and scheduling dependency upgrades. Select workloads that exercise the changed path; documentation-only and test-only changes do not require unrelated performance runs.
- Correctness stress tests must validate every response against its originating request, explicit-submission response order, and exactly-once handling; completion counts alone are insufficient. Include concurrent single, explicit-batch, and mixed submissions when changing shared submission, scheduling, or completion logic.
- For critical-path changes, record the commands, runtime/configuration, workload, correctness outcome, and relevant time/allocation conclusions in the appropriate evaluation summary. Keep detailed measurements and raw run output local, and link the reusable workloads and reproduction commands from the summary. Performance optimizations require a comparable baseline and measured results before committing; a benchmark smoke run alone does not demonstrate a gain. Keep the benchmark index current when adding or changing stress workloads.
- Add or update deterministic unit tests for behavior changes, especially batching, concurrency, cancellation, backpressure, exception fan-out, and shutdown.
- Use strict Moq mocks for interaction boundaries. Prefer stateful test collaborators only when a mock would obscure concurrency or lifecycle behavior.
- Unit tests must not require network access or external services.
- Keep OpenTelemetry sample validation sessions bounded and shut down their Docker Compose stack immediately after
collecting the required evidence, including on failure or cancellation. Follow the cleanup instructions in
samples/RequestBatcher.Observability/AGENTS.md; do not leave the stack running during unrelated work or after the task finishes unless the user explicitly requests it. - Keep database performance comparisons in
tests/RequestBatcher.Benchmarks, not in the unit-test project. Benchmark container startup, schema creation, pool warmup, and table reset must stay outside measured regions. - Keep direct and batched database paths comparable: use the same logical requests, schema, connection pool limit, commit semantics, and validation. Verify persisted row counts so a faster result cannot hide dropped work.
- Partition-key tests must prove both sides of the contract: equal keys route consistently while separate batches for equal keys can execute concurrently. Duplicate-merging examples must remain correct when duplicates span multiple batches.
- Batch-submission tests must cover all-or-nothing admission, capacity waiting, handler failure, and per-item partition routing. A submitted batch must not be documented as one handler invocation.
- Name tests
MemberOrBehavior_Scenario_ExpectedOutcome, with PascalCase within each segment and underscores between the three segments. - Avoid timing-only assertions where a completion source or another deterministic synchronization primitive can express the same condition.
- After C# changes, run the narrowest relevant tests while iterating, then the complete test project and formatting.
In a dirty worktree, pass
--includewith the C# files changed by the task so formatting does not modify unrelated work. CI remains responsible for verifying that the committed tree needs no formatting changes.
Standard checks from the repository root:
dotnet restore RequestBatcher.slnx
dotnet format RequestBatcher.slnx --no-restore
dotnet build RequestBatcher.slnx --configuration Release --no-restore
dotnet test tests/RequestBatcher.Tests/RequestBatcher.Tests.csproj --configuration Release --no-build --no-restore
dotnet pack src/RequestBatcher/RequestBatcher.csproj --configuration Release --no-build --no-restore
dotnet pack src/RequestBatcher.OpenTelemetry/RequestBatcher.OpenTelemetry.csproj --configuration Release --no-build --no-restore
docker compose -f samples/RequestBatcher.Deduplication/compose.yaml up -d
dotnet run --project samples/RequestBatcher.Deduplication/RequestBatcher.Deduplication.csproj --no-build --no-restoreThe PostgreSQL and Redis benchmarks are opt-in and require a running Docker daemon:
dotnet run --project tests/RequestBatcher.Benchmarks/RequestBatcher.Benchmarks.csproj \
--configuration Release -- \
--filter '*PostgreSqlWriteBenchmarks*'
dotnet run --project tests/RequestBatcher.Benchmarks/RequestBatcher.Benchmarks.csproj \
--configuration Release -- \
--filter '*Redis*Benchmarks*'