Copilot Instructions for .NET / C# Projects
GitHub Copilot.github/copilot-instructions.mdCoding
C# conventions for Copilot: nullable reference types treated as errors, Async suffix on every awaitable method, CancellationToken plumbing, and xUnit patterns that scale.
Use when: Use in a .NET 8+ solution when you want Copilot suggestions to respect nullable annotations, async conventions, and your xUnit test layout instead of producing 2015-era C#.
Get this skill
Add the skill file with the CLI, by hand, or over MCP.Save this to your project or home directory so GitHub Copilot can load it.
npx goodprompts add copilot-dotnet-instructionsSaves to
./.github/copilot-instructions.mdnpx goodprompts add copilot-dotnet-instructionsSaves to
./.github/copilot-instructions.mdSkill file
.github/copilot-instructions.md3,083 characters# .NET Project Instructions
## Nullable reference types
- `<Nullable>enable</Nullable>` and `<WarningsAsErrors>nullable</WarningsAsErrors>` are set solution-wide. Never suggest code that introduces a nullable warning.
- The null-forgiving operator `!` is allowed only in test arrange blocks and DI wire-up; in production code, handle the null or change the signature.
- Public method parameters that must not be null get a guard: `ArgumentNullException.ThrowIfNull(arg);` as the first line. Do not write `if (arg == null) throw ...` long-form.
- Prefer returning empty collections over null. A method returning `List<T>?` is almost always wrong — return `IReadOnlyList<T>` and `[]`.
- Model "value may be missing" with a nullable annotation, not magic defaults like `string.Empty` or `-1`.
## Async conventions
- Every method returning `Task`/`Task<T>`/`ValueTask` is named with the `Async` suffix, including private methods. No exceptions for controllers.
- Every public async method accepts a `CancellationToken cancellationToken = default` as its last parameter and passes it to every awaited call that accepts one.
- Never use `.Result`, `.Wait()`, or `.GetAwaiter().GetResult()` — if you are in a sync context that needs async work, make the caller async instead.
- `async void` is allowed only for event handlers; everywhere else return `Task`.
- Use `await foreach` with `IAsyncEnumerable<T>` for streamed reads; do not buffer a whole table into a list just to iterate it.
## Language and style
- File-scoped namespaces, `var` when the type is apparent, target-typed `new()` where the type appears on the left.
- Records for DTOs and messages; classes for entities and services. DTO records are `sealed`.
- Pattern matching (`is`, `switch` expressions) over chains of `if`/`else if` on type or enum.
- LINQ method syntax only; no query syntax. Avoid multiple enumeration — materialize with `.ToList()` once if you iterate twice.
- Exceptions: throw the most specific BCL type that fits; custom exceptions only when callers need to catch them specifically.
## xUnit patterns
- Test classes mirror the source layout: `OrderService` in `src/Billing/` is tested by `OrderServiceTests` in `tests/Billing.Tests/`.
- Test names follow `MethodName_Scenario_ExpectedOutcome`, e.g. `RefundAsync_AlreadyRefunded_ThrowsInvalidOperation`.
- One logical assert per test; use `Theory` with `InlineData`/`MemberData` instead of copy-pasted `Fact`s that differ by one value.
- Shared expensive setup goes in an `IClassFixture<T>`; never static mutable state between tests.
- Mock only external boundaries (clock, HTTP, message bus) with NSubstitute; never mock the class under test's own collaborators if a real instance is cheap.
- Async tests return `Task` and await the act step — never `async void` tests.
## Workflow
- Run `dotnet build -warnaserror` and `dotnet test` before suggesting a commit.
- Never edit files under `obj/`, `bin/`, or `*.Designer.cs`.
- New projects target the same TFM as the solution; do not add a package reference when the BCL already covers the need.