Copilot Instructions for .NET / C# Projects
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#.
The skill file
GitHub Copilot · .github/copilot-instructions.md.github/copilot-instructions.md3083 chars
# .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.
Install
drop into your repoSave this to your project or home directory so GitHub Copilot can load it.
path./.github/copilot-instructions.md
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…