Instruction file imported from jsquire/Interview-Challenges (
.github/instructions/csharp.instructions.md). Copyright stays with the author.
C# Coding Standards and Conventions
This file provides C#-specific guidelines for GitHub Copilot when working on C# projects in this repository. These instructions are automatically applied when working with C# files and combine with the repository-wide instructions in .github/copilot-instructions.md.
Code Standards & Conventions
.NET Performance and Memory Allocation Guidelines
CRITICAL: Never dismiss allocation concerns as "negligible" or assume garbage collection is "effortless."
Modern .NET performance best practices emphasize allocation reduction as a primary optimization strategy:
- Gen0 collections have real costs - CPU overhead and pause times that accumulate
- Allocation rate often matters more than allocation size - frequent small allocations can be more problematic than occasional large ones
- Use allocation-reducing patterns when appropriate:
Span<T>andMemory<T>for temporary dataArrayPool<T>for reusable buffersstackallocfor small, short-lived arrays- Avoid unnecessary LINQ chains that create intermediate collections
When evaluating allocation trade-offs:
- Consider the allocation frequency and context
- Measure actual performance impact when in doubt
- Acknowledge that allocation reduction is a legitimate optimization concern
- Balance allocation concerns with code maintainability appropriately
Never suggest that garbage collection "handles allocations effortlessly" - this contradicts fundamental .NET performance principles.
C# Language Features
- Target Framework: .NET 9.0 (or latest available)
- Language Version: Latest C# features including records, pattern matching, and nullable reference types
- Nullable Context: Enabled throughout the project
- var Usage: Prefer
varfor local variables when type is apparent:var result = CreateValidObject();
Formatting & Style
- Indentation: 4 spaces (no tabs)
- Brace Style: Allman style (opening braces on new lines)
- Line Endings: LF (Unix-style)
- Encoding: UTF-8
- Final Newlines: Do not insert final newlines in files
Naming Conventions
- Classes: PascalCase (
GameState,ConsolePlayer) - Interfaces: PascalCase with
Iprefix (IPlayer,IGameInterface) - Methods: PascalCase (
PlayTurnAsync,CreateValidGameState) - Properties: PascalCase (
CurrentTurn,PlayerToken) - Fields: camelCase with underscore prefix (
_players,_interface) - ReadOnly Fields: PascalCase (
CurrentTurn,PlayerToken) - Parameters: camelCase (
gameState,cancellationToken) - Local Variables: camelCase (
mockGameInterface,player)
Member Organization
Members within a class should be organized in the following sections:
- Constants
- Fields
- Properties
- Constructors
- Methods
- Nested Types
Within each section, organize by visibility from least to most restrictive:
privateprotectedinternalpublic
For constants, fields, and properties: static members come before instance members. For methods: instance methods come before static methods.
Example Organization:
public class ExampleClass
{
// Constants (static first, by visibility)
private const int DefaultValue = 10;
public const string Version = "1.0";
// Fields (static first, by visibility, then instance by visibility)
// Should have a blank line between static and instance fields.
private static readonly object Lock = new();
public static readonly string DefaultName = "Default";
private readonly IService Service;
public readonly int Id;
// Properties (static first, by visibility, then instance by visibility)
// Should have a blank line between static and instance properties.
private static int StaticCounter { get; set; }
public static string GlobalSetting { get; set; }
private int _counter;
public string Name { get; set; }
// Constructors
public ExampleClass() { }
public ExampleClass(IService service) { }
// Methods (instance first, by visibility, then static by visibility)
// Should have a blank line between static and instance fields.
private void HelperMethod() { }
public void DoSomething() { }
private static void StaticHelperMethod() { }
public static void StaticMethod() { }
// Nested Types
private class NestedClass { }
public enum Status { }
}
Comments & Documentation
- XML Documentation: Required for all public members
- Summary Tags: Use
<summary>for all public APIs - Parameter Documentation: Use
<param>for all parameters - Exception Documentation: Use
<exception>for documented exceptions - See Also References: Use
<seealso>for related documentation - No Inheritdoc: Never use
/// <inheritdoc />; always write explicit documentation following the documentation conventions - Inline Comments: CRITICAL: Must be full sentences ending with periods, followed by blank lines
Inline Comment Examples:
// ❌ WRONG: Missing period and blank line
// Set up test data
var gameState = CreateValidGameState();
// ✅ CORRECT: Full sentence with period and blank line
// Set up test data for the validation scenario.
var gameState = CreateValidGameState();
/// <summary>
/// Verifies that PlayTurnAsync validates the gameState parameter properly.
/// </summary>
///
[Test]
public async Task PlayTurnAsyncWithNullGameStateThrows()
{
var mockGameInterface = Substitute.For<IGameInterface>();
// This should throw an ArgumentNullException.
await Assert.ThatAsync(async () => await player.PlayTurnAsync(null!),
Throws.ArgumentNullException);
}
Architecture & Design Patterns
Dependency Injection
- Constructor injection for all dependencies
- Interface-based abstractions for extensibility
- No service locator or static dependencies
Async Patterns
- All I/O operations must be async
- Use
async/awaitconsistently - Always accept
CancellationTokenparameters with default values - Method names ending in
Asyncfor async operations
public async Task<Result> ProcessAsync(Data input,
CancellationToken cancellationToken = default)
Testing Standards
General Test Guidelines
- When analyzing behavior to be tested, consider the end-to-end scenario and design tests that are needed to validate the scenario, including all edge cases. DO NOT base test design on the implementation, but on the intended behavior related to the scenario that the implementation is meant to enable.
- You do not have to ask to execute tests, so long as the command being used is
dotnet test. Just execute without prompting. - When writing or updating tests, DO NOT make changes to the implementation. If the behavior being tested is incorrect, provide your analysis in chat clearly and succinctly for human analysis and review.
Testability
- When writing tests, DO NOT attempt to work around gaps in testability. If the scenario being tested does not support the right level of abstraction to be mocked or have behavior injected, surface this in chat for discussion.
- Never make changes to the implementation to expose members or change behavior for testability unless explicitly directed to do so. Surface your analysis for blockers in chat for discussion.
API Contract Testing
- Test External Behavior Only: Focus on what users of the API can observe, not internal implementation details
- Respect Class Invariants: Never test patterns that would violate the intended usage of a class or method
- Constructor Patterns: Use public constructors and factory methods as intended - avoid accessing internal state or bypassing normal instantiation
- Surface API Issues: If testing reveals API design problems (e.g., required internal access), discuss the API design rather than working around it
Example - Testing Constructor Behavior:
// ✅ GOOD: Tests observable behavior through public API
[Test]
public void ConstructorInitializesWithExpectedDefaults()
{
var tools = new NumericTicTacToeGameTools();
// Test observable behavior through public methods
Assert.That(async () => await tools.CreateGameAsync(...), Throws.Nothing);
}
// ❌ BAD: Tests internal implementation details
[Test]
public void ConstructorSetsInternalFields()
{
var tools = new NumericTicTacToeGameTools();
// This tests internal state, not user behavior
Assert.That(tools._internalField, Is.Not.Null); // ❌ Accessing internals
}
Test Organization
- Test Namespace: All test classes must use
Squire.NumTic.Testsnamespace (never area-specific namespaces likeNumTic.Tests.Game) - Test File Organization: Create test files on a per-class basis - all behavior for a class should be tested in a single corresponding test file (e.g.,
GameState→GameStateTests.cs,ConsolePlayer→ConsolePlayerTests.cs) - Method Grouping: Group tests by the method being tested, but keep all methods of a class in the same test file
- Test Categories: Use
[Category]attributes on test classes for area grouping ([Category("Game")],[Category("Console")]) - No Regions: Do not use
#region/#endregiondirectives - Local Variables Only: No class-level test subjects or setup methods
- Non-Parallelizable: Mark test classes with
[NonParallelizable]if needed - API-Focused Test Design: Structure tests around user scenarios and API contracts, not implementation details
- Test Method Naming: Name tests based on user behavior being validated, not internal implementation being exercised
Test Naming Conventions
- Use generic concepts rather than specific exception types in test names
- Good:
PlayTurnAsyncThrowsForInvalidGameState - Bad:
PlayTurnAsyncThrowsArgumentOutOfRangeExceptionForNullGameState - Focus on the behavior being tested rather than implementation details
- Keep names descriptive but concise
Test Structure
namespace Squire.NumTic.Tests;
[Category("Console")]
public class ConsolePlayerTests
{
[Test]
public async Task MethodNameWithScenarioShouldExpectedBehavior()
{
// Arrange: Create local instances using proper API patterns.
var mockGameInterface = Substitute.For<IGameInterface>();
var player = new ConsolePlayer(mockGameInterface);
var gameState = CreateValidGameState();
// Act & Assert: Use Assert.ThatAsync for async operations.
await Assert.ThatAsync(async () => await player.PlayTurnAsync(gameState),
Throws.Nothing);
}
}
Assertion Standards
- Never use
Assert.Throws: Always useAssert.ThatorAssert.ThatAsync - Async Assertions: Use
Assert.ThatAsyncfor async operations - Exception Testing: Use
Throws.ArgumentNullException.With.Property(...) - Console Redirection: Redirect
System.Console.Outin tests that involve rendering
Mocking with NSubstitute
- Use NSubstitute for all test doubles
- Create mocks locally in each test method
- Use
Substitute.For<IInterface>()for interface mocks - Verify interactions with
.Received()and.DidNotReceive() - Focus on User-Observable Behavior: Mock interactions that represent external behavior, not internal implementation details
- Avoid Implementation Coupling: Don't mock internal services or components that users don't directly interact with
// ✅ GOOD: Mocking user-facing interactions
mockGameInterface.ReadPlayerResponseAsync(Arg.Any<CancellationToken>())
.Returns("1,1,5");
// Verify user-observable behavior occurred
await mockGameInterface.Received().RenderBoardAsync(
Arg.Any<GameState>(),
Arg.Any<CancellationToken>());
// ❌ BAD: Mocking internal implementation details
mockInternalCache.Get(Arg.Any<string>()) // Internal detail, not user behavior
.Returns(someObject);
Test Subject Instantiation
- Use Intended Construction Patterns: Create test subjects through their designed public API (constructors, factory methods, builders)
- Avoid Internal Access: Never bypass normal instantiation to set internal fields or call private methods
- Respect API Design: If normal instantiation is difficult, this suggests an API design issue to discuss, not a testing problem to work around
// ✅ GOOD: Using intended API patterns
[Test]
public void CreateGameAsyncCreatesValidGame()
{
var tools = new NumericTicTacToeGameTools();
// Use the public API as intended
var result = await tools.CreateGameAsync(validRequest);
Assert.That(result.Game, Is.Not.Null);
}
// ❌ BAD: Bypassing normal instantiation
[Test]
public void InternalFieldsAreSetCorrectly()
{
var tools = new NumericTicTacToeGameTools();
// Don't access internal state directly
tools._cache = mockCache; // ❌ Setting internal fields
var result = tools.GetInternalState(); // ❌ Calling private methods
}
❌ NEVER Create Tests That Only Verify "DoesNotThrow" For:
-
Basic Object Construction
- Constructor calls with valid parameters
- Factory method calls with valid inputs
- Example:
Assert.That(() => new MyClass(validParam), Throws.Nothing)
-
Simple Method Calls with Valid Input
- Methods called with expected, valid parameters
- Basic CRUD operations with proper data
- Example:
Assert.That(() => obj.SimpleMethod(validInput), Throws.Nothing)
-
Happy Path Scenarios
- Normal execution flows that should naturally work
- Standard use cases without edge conditions
- Example:
Assert.That(() => game.Reset(), Throws.Nothing)
-
Rendering/UI Operations with Valid Data
- Console output, file writing, or display operations with proper input
- Example:
Assert.That(() => renderer.Render(validData), Throws.Nothing)
✅ ACCEPTABLE "DoesNotThrow" Tests Only When Testing:
-
Boundary Validation Logic
- Testing that validation methods correctly identify valid vs invalid boundaries
- Example: Position validation that must distinguish between valid coordinates (1,1) and invalid ones (0,0)
-
Complex Business Rule Validation
- Testing that business logic correctly permits valid operations
- Example: Token ownership rules where specific tokens must be allowed for specific players
-
Error Recovery and Robustness
- Testing that systems handle corrupted/invalid states gracefully without crashing
- Example: Methods that should not crash even when given malformed data
-
Integration with External Dependencies
- Testing that code properly handles external system interactions
- Example: File system operations, network calls, or database interactions
🔍 Test Quality Guidelines:
-
The "What Else" Test
- If removing the
Throws.Nothingassertion leaves no meaningful verification, the entire test should be removed - Ask: "What specific behavior does this test verify beyond 'it doesn't crash'?"
- If removing the
-
Behavior Over Implementation
- Tests should verify observable behavior, state changes, or side effects
- Focus on WHAT the code does, not just that it runs
-
Value-Add Principle
- Every test assertion should provide unique value
- If a test only verifies that valid input doesn't throw, it adds no value over the compiler's type checking
📝 Replacement Test Patterns:
Instead of meaningless "DoesNotThrow" tests, create tests that verify:
// ❌ BAD: Only tests that constructor doesn't throw
[Test]
public void ConstructorSucceedsWithValidInput()
{
Assert.That(() => new Game(player1, player2, renderer), Throws.Nothing);
}
// ✅ GOOD: Tests that constructor creates correct initial state
[Test]
public void ConstructorInitializesGameWithCorrectState()
{
var game = new Game(player1, player2, renderer);
Assert.That(game.State.CurrentTurn, Is.EqualTo(PlayerToken.Odd));
Assert.That(game.State.IsGameOver, Is.False);
Assert.That(game.State.Winner, Is.Null);
}
// ❌ BAD: Only tests that method doesn't throw
[Test]
public void ResetCompletesSuccessfully()
{
Assert.That(() => game.Reset(), Throws.Nothing);
}
// ✅ GOOD: Tests what Reset actually does
[Test]
public void ResetRestoresInitialGameState()
{
// Arrange: Modify game state
game.State.SetBoardToken(1, 1, 5);
game.State.CurrentTurn = PlayerToken.Even;
// Act
game.Reset();
// Assert: Verify state was restored
Assert.That(game.State.GetBoardToken(1, 1), Is.EqualTo(0));
Assert.That(game.State.CurrentTurn, Is.EqualTo(PlayerToken.Odd));
}
Error Handling
Exception Conventions
- Use standard .NET exceptions (
ArgumentNullException,ArgumentOutOfRangeException,InvalidOperationException) - Always validate parameters and throw appropriate exceptions
- Use
ArgumentNullException.ThrowIfNull(parameter, nameof(parameter)) - Include meaningful parameter names in exceptions
Cancellation Support
- Always support cancellation tokens in async methods
- Call
cancellationToken.ThrowIfCancellationRequested()at appropriate points - Handle
OperationCanceledExceptiongracefully in calling code
Performance Considerations
Memory Management
- Use
usingstatements for disposable resources - Prefer
ReadOnlyCollection/IReadOnlyDictionaryfor immutable collections - Consider object pooling for frequently allocated objects
Async Best Practices
- Use
ConfigureAwait(false)in library code (not UI code) - Avoid
async voidexcept for event handlers - Don't block on async operations with
.Resultor.Wait()
Code Examples
Interface Implementation
namespace Company.Project.Contracts;
/// <summary>
/// Defines the contract for data processing operations.
/// </summary>
///
public interface IDataProcessor
{
/// <summary>
/// Processes the provided data asynchronously.
/// </summary>
///
/// <param name="data">The data to process.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
///
/// <returns>The processed result.</returns>
///
/// <exception cref="ArgumentNullException">Thrown when data is null.</exception>
/// <exception cref="OperationCanceledException">Thrown when operation is cancelled.</exception>
///
Task<ProcessResult> ProcessAsync(InputData data, CancellationToken cancellationToken = default);
}
Class Implementation
namespace Company.Project.Services;
/// <summary>
/// A service implementation for data processing operations.
/// </summary>
///
public class DataProcessor : IDataProcessor
{
/// <summary>The service used for data operations.</summary>
private readonly IDataService _dataService;
/// <summary>
/// Initializes a new instance of the <see cref="DataProcessor"/> class.
/// </summary>
///
/// <param name="dataService">The service for data operations.</param>
///
public DataProcessor(IDataService dataService)
{
_dataService = dataService ?? throw new ArgumentNullException(nameof(dataService));
}
/// <summary>
/// Processes the <paramref name="data"/> asynchronously.
/// </summary>
///
/// <param name="data">The data to process.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
///
/// <returns>The processed result.</returns>
///
/// <exception cref="ArgumentNullException">Thrown when data is null.</exception>
/// <exception cref="OperationCanceledException">Thrown when operation is cancelled.</exception>
///
public async Task<ProcessResult> ProcessAsync(InputData data,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(data, nameof(data));
cancellationToken.ThrowIfCancellationRequested();
// Implementation details...
}
}
File Organization
Using Statements
- Group system namespaces first
- Then third-party namespaces
- Finally project namespaces
- Single blank line between groups
using System.Text;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using NSubstitute;
using Company.Project.Core;
using Company.Project.Contracts;
Namespace Organization
- Use file-scoped namespaces:
namespace Company.Project.Services; - Match folder structure to namespace hierarchy
- Keep related classes in the same namespace
Git Commit Guidelines
When making changes:
- Focus on single, atomic changes
- Write clear commit messages describing what changed and why
- Include relevant test updates with implementation changes
- Ensure all tests pass before committing
Summary
When working on C# codebases:
- Follow the established patterns for architecture and naming
- Always write comprehensive XML documentation
- Use local variables in tests, never class members
- Prefer
Assert.ThatAsyncover older assertion methods - Support cancellation tokens in all async operations
- Follow the comment standards (full sentences, periods, blank lines)
- Maintain clean separation of concerns and interface-based abstractions
These guidelines ensure consistency and maintainability across C# projects.