Use this file to follow our coding guidelines when contributing to Darker. Darker is the query-side
counterpart of Brighter, and the two projects share
their conventions and workflow. The same guidance is restated for coding agents in
.agent_instructions/; if you work with an agent, also read
AGENTIC_CODING.md.
- How to Use This File
- First Time Contributing?
- Architecture Decision Records
- Code Style
- Testing
- Documentation
- Dependency Management
- Making Changes
- Support for Agentic Coding
- Project Structure
Welcome! Here's how to get started:
- Fork and clone the repository
- Ensure you have the .NET 8 and .NET 9 SDKs installed:
dotnet --list-sdks - Build the solution:
dotnet build Darker.Filter.slnf - Run the tests (no external dependencies needed):
dotnet test Darker.Filter.slnf
Darker.Filter.slnf excludes the MAUI sample app, which needs extra workloads. Use Darker.slnx
only if you are working on that sample.
- Look through the open issues for one you would like to pick up
- Comment on the issue to let others know you're working on it
- Read the relevant sections below:
- Code Style - Understand our conventions
- Testing - Learn our TDD approach
- Documentation - XML doc requirements
- Make your changes following our guidelines
- Submit a PR targeting the
masterbranch
- ✅ Use TDD where possible (write tests first)
- ✅ Prefer real, Simple or InMemory implementations over mocks in tests
- ✅ Add XML documentation to all public APIs
- ✅ Constants use
ALL_CAPSnaming convention - ✅ Include license header:
#region Licence(British spelling) - ✅ Use Conventional Commits for commit messages
- ✅ Keep the sync and async pipelines in step — a change to one usually needs its twin
- If adding a new capability to Darker, write an Architecture Decision Record (ADR) in
docs/adr/.- The ADR should focus on the why of your decision, over implementation details, which can be better found in the code.
- Use the ADR to agree what you want to do, before you do it.
- Use the ADR to signal to others, including future maintainers, why Darker is built the way it is.
- Follow the skeleton, readability rules and diagram rules in documentation.md § Architecture Decision Records. ADR 0001 is the minimal template.
- Every ADR starts with a short YAML frontmatter block (
id,title,status,author,created,summary,tags), described in adr_frontmatter.md.docs/adr/index.mdis generated from that frontmatter, so read it to find related decisions, but never edit it by hand.
You can create the ADR as the first step on a new branch or fork. Your first commit then contains the ADR describing the change, which lets you open a draft PR so others can check their understanding of the change and give feedback early, before expectations drift apart. If you change the design as you learn, add another ADR that supersedes the old one.
- Follow .NET C# conventions
- Use Microsoft's C# naming conventions for identifiers
- For a const, use an All Caps naming convention, with underscores between words i.e.
MAX_RETRY_COUNT. This replaces rules in the Microsoft C# naming convention. - Follow Microsoft's C# coding conventions
- DO NOT use Microsoft's Framework Design Guidelines. They are not idiomatic and outdated.
- Prefer expression-bodied members for simple properties and methods.
- Prefer primary constructors where possible, especially for simple classes and records.
- Use readonly for fields that do not change after construction.
- Enable nullable reference types (
<Nullable>enable</Nullable>), and make types nullable to indicate optionality. - You may use marker interfaces. We find them useful as a base type for async and sync interfaces.
- We support both sync and async pipelines.
- Suffix async methods with
Async. - Handlers, decorators and their attributes come in separate sync and async types (see ADR 0008). When you change one path, change its twin, or say why not.
- Suffix async methods with
- Use assemblies to provide modularity, divided by optionality.
- Required behavior lives in
Paramore.Darker. - Optional behavior, such as DI integration, diagnostics, validation providers or caching, lives in its own assembly, so users only take a dependency on the NuGet packages they need.
- Required behavior lives in
- Default to a class per source file, unless one class clearly exists as the details of another.
- Use Responsibility-Driven Design — see design_principles.md.
- Specify object behavior before object structure.
- Think of responsibilities for "knowing", "doing" and "deciding", and allocate them to roles.
- Distribute behavior: make objects smart, not just holders of data.
- Preserve flexibility: design objects so interior details can readily change.
- Avoid primitive obsession. Where a primitive could be replaced with a more expressive type, use a class, struct or record.
- Follow Beck's "Tidy First" approach:
- Separate structural changes (renaming, extracting methods, moving code) from behavioral changes.
- Never mix the two in the same commit, and make structural changes first.
- Run the tests before and after a structural change to show behavior has not changed.
- Not all of our code follows these conventions yet. Follow the boy scout rule and fix what you touch.
The full guidance is in code_style.md.
- Use TDD where possible.
- Write developer tests using xUnit, with Shouldly for assertions.
- Name test methods
When_[condition]_should_[expected_behavior], and name the file after the test method. Name test classes[Behavior]Tests, never withWhen_. - Prefer one test case per file.
- Ensure all new features and bug fixes include appropriate test coverage.
- We write developer tests: a failing test implicates the most recent edit. We do not use mocks to
isolate the system under test.
- You might want to watch this video to understand our preferred testing approach, or review TDD Revisited.
- Where possible, we are test first: Red, Green, Refactor.
- Only write the code a test needs to pass; do not write speculative code.
- Tests confirm behavior, not implementation details. It should be possible to refactor without
breaking tests.
- Use Arrange/Act/Assert, made explicit with comments.
- Use the Evident Data pattern: highlight the state that affects the outcome.
- The trigger for a new test is a new behavior, not a new method.
- Only test exports from an assembly: public methods on public classes.
- Do not make something public just to test it, and never use
InternalsVisibleTo. If no existing export reaches the behavior, read Narrow and deep in testing.md before widening the surface.
- Do not make something public just to test it, and never use
Prefer, in this order:
- Real instances — for example
QueryHandlerRegistryorInMemoryQueryContextFactory. - Simple implementations — delegate-based, such as
SimpleHandlerFactoryandSimpleHandlerDecoratorFactory. - InMemory implementations — such as
InMemoryDecoratorRegistry. - Mocks — a last resort, only for I/O boundaries or interactions that cannot be observed through behavior.
Put test-specific handlers, queries and decorators in the test project's TestDoubles/ directory
(for example test/Paramore.Darker.Core.Tests/TestDoubles/), one class per file. The full guidance
is in testing.md.
- Add or update XML documentation comments (
///) for all exports from assemblies: public and protected members of public types. Do not document internal or private members. - Write for a developer reading IntelliSense: helpful, but short enough that they will read it.
<summary>— what the type or member is for. Use<paramref>to refer to parameters.<param>— what the parameter is for, its effect, and any default if it is optional.<returns>,<typeparam>,<exception>and<value>as appropriate, with<see cref=""/>for types.<remarks>— implementation notes or design decisions a maintainer would want to know.
- Prefer intention-revealing names to inline comments. In tests you may use
//Arrange,//Act,//Assert. - Update documentation comments when APIs change.
- Document new features in the Docs repository of the BrighterCommand organization.
The full guidance, including the ADR contract and writing tone for design documents, is in documentation.md.
- We add an MIT license comment to the top of every source file, before any
usingstatements. - Put it in a
#region Licenceblock (British spelling, no space). - Add your name and the year for a new file. An agent should use the name of the contributor directing it.
#region Licence
/* The MIT License (MIT)
Copyright © [Year] [Your Name] [Your Contact Email]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. */
#endregion- We use Central Package Management: package versions live in
Directory.Packages.props, and project files reference packages without a version.
<!-- Directory.Packages.props -->
<ItemGroup>
<PackageVersion Include="Polly" Version="8.7.0" />
</ItemGroup>
<!-- A project file -->
<ItemGroup>
<PackageReference Include="Polly" />
</ItemGroup>- Align all
Microsoft.Extensions.*andSystem.*package versions. - Avoid mixing preview and stable package versions.
- Keep the core
Paramore.Darkerpackage's dependencies to a minimum; an optional dependency belongs in an optional assembly.
- Make sure you have the latest version of
master. - Raise an issue in the GitHub issue tracker, after checking whether someone has already raised it.
- If you want to add a feature, as opposed to fixing something that is broken, still raise an issue. It lets us give specific advice and check the feature does not conflict with work in progress.
- Comment on an issue when you pick it up, so everyone else knows.
- If you have a defect, include:
- Steps to reproduce the issue
- A failing test if possible
- The stack trace for any errors you encountered
# Build (excludes the MAUI sample)
dotnet build Darker.Filter.slnf
# Run all tests — none need external services
dotnet test Darker.Filter.slnf
# Run one test project
dotnet test test/Paramore.Darker.Core.Tests
# Run a single test
dotnet test test/Paramore.Darker.Core.Tests --filter "FullyQualifiedName~When_query_handler_not_registered"Ensure all tests pass before submitting a PR. CI builds and tests against .NET 8 and .NET 9.
- Try to write a good commit message.
- Use the imperative mood (e.g., "Add support for X", "Fix bug in Y").
- Reference issues or PRs where relevant.
- Use Conventional Commits
(
feat:,fix:,refactor:,test:,docs:,chore:).
If your change breaks an existing behavior or public interface, add a section for it under
## Master in release_notes.md, saying what breaks and how to migrate. The
/spec:write_release_notes command drafts this section from the ADRs for a spec.
| Branch | Description |
|---|---|
master |
The tip of active development. Anything in master should ship in the next release, and must compile and pass tests. |
| Release branches | The code for an actively supported release, created when master needs breaking changes the current release cannot take. |
| Other branches | Work that is not ready for master (for example, it would break CI) or is experimental. |
When submitting a pull request: target master unless you're fixing a bug in a specific release
branch.
- Fork the project and clone your fork
- Create a branch for your work
- Push to your fork
- Submit a pull request against
master - Respond to review feedback; we will get to it as soon as we can
Please ask that your pull request is merged with a merge commit, not squashed or rebased. Some of
our tooling pins commit shas from a merged branch's history for later reference — for example,
/spec:review code records the commit it reviewed in the spec's review-code.md — and only a merge
commit keeps those commits reachable from master afterwards.
To safeguard the project, we ask you to sign a Contributor License Agreement. You keep your copyright, but grant the project the right to use your contribution in perpetuity, so that the project is not at risk from any one contributor withdrawing their grant of license.
The agreement is in CLA.txt, and signing works through GitHub: sign the Contributor License Agreement.
We welcome code authored with a coding agent. However, you are responsible for the code you submit: review it, understand it, and make sure the agent followed these guidelines.
Our agent support is focused on Claude Code, though other agents can use the same instructions in
.agent_instructions/. See AGENTIC_CODING.md for:
- the instruction files and slash commands we provide;
- the
/specworkflow for features: requirements, ADRs, adversarial review, tasks, TDD implementation, and a code review of the branch; - how to choose a review gear with
/spec:gear, and whether to work a task at a time with/spec:implementor run a loop with/spec:ralph-implement; - the
/bugfixworkflow, which proves a bug's root cause before fixing it.
Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. The code of conduct is from the Contributor Covenant.
src/— the libraries, one assembly per responsibility:Paramore.Darker— the core:QueryProcessor, the pipeline builder, registries and factories, the built-in decorators (query logging, retry and fallback policies, Polly resilience pipelines), streaming queries, and observability.Paramore.Darker.Extensions.DependencyInjection— integration withMicrosoft.Extensions.DependencyInjection, including handler assembly scanning.Paramore.Darker.Extensions.Diagnostics— wires Darker's tracing and metrics into OpenTelemetry.Paramore.Darker.Caching— the query result caching decorator.Paramore.Darker.Validation— the provider-agnostic validation decorator, withParamore.Darker.Validation.DataAnnotationsandParamore.Darker.Validation.FluentValidationproviders.Paramore.Darker.Testing— helpers for testing code that uses Darker.
test/— test projects, partitioned to match the libraries (Paramore.Darker.Core.Tests,Paramore.Darker.Extensions.Tests,Paramore.Darker.Caching.Tests, the validation test projects, and so on), plusParamore.Darker.Tests.AOTfor trimming/AOT compatibility, andParamore.Darker.Benchmarks.samples/—SampleMinimalApi(an ASP.NET Core minimal API) andSampleMauiTestApp.docs/adr/— Architecture Decision Records, with a generatedindex.md.specs/— specifications produced by the/specworkflow.