--- id: csharp name: C# Standards description: "Universal C# best practices for naming, async, LINQ, error handling, and testing" category: core type: standard version: 1.1.0 author: community --- # Universal C# Standards **Purpose**: Universal C# best practices for AI agents working on .NET projects **Scope**: Language-level patterns, not framework-specific **Last Updated**: 2026-03-15 --- ## Table of Contents 1. [Naming Conventions](#1-naming-conventions) 2. [Type Safety & Nullability](#2-type-safety--nullability) 3. [Async/Await Patterns](#3-asyncawait-patterns) 4. [LINQ](#4-linq) 5. [Error Handling](#5-error-handling) 6. [Pattern Matching](#6-pattern-matching) 7. [Code Organization](#7-code-organization) 8. [Records & Immutability](#8-records--immutability) 9. [Dependency Injection](#9-dependency-injection) 10. [Testing](#10-testing) --- ## 1. Naming Conventions ### 1.1 General Rules | Element | Convention | Example | |---------|-----------|---------| | Classes, structs, records | PascalCase | `UserService`, `OrderItem` | | Interfaces | `I` + PascalCase | `IUserRepository`, `IOrderService` | | Methods | PascalCase | `GetUserById`, `ProcessOrder` | | Properties | PascalCase | `FirstName`, `CreatedAt` | | Fields (private) | `_` + camelCase | `_logger`, `_repository` | | Local variables | camelCase | `userId`, `orderTotal` | | Parameters | camelCase | `userId`, `cancellationToken` | | Constants | PascalCase | `MaxRetryCount`, `DefaultTimeout` | | Enums | PascalCase (type and members) | `OrderStatus.Pending` | | Async methods | `Async` suffix | `GetUserAsync`, `SaveOrderAsync` | ### 1.2 Method Naming ```csharp // ✅ GOOD - Verb + noun, PascalCase public async Task GetUserByIdAsync(Guid userId) { } public async Task> ListOrdersAsync(Guid userId) { } public async Task DeleteUserAsync(Guid userId) { } public bool IsEligibleForDiscount(Order order) { } public bool HasPermission(string action) { } // ❌ AVOID - Ambiguous or wrong case public async Task fetchuser(Guid id) { } public async Task DoUserGet(Guid id) { } ``` ### 1.3 Interface Naming ```csharp // ✅ GOOD - Always prefix with I public interface IUserRepository { } public interface IOrderService { } public interface INotificationSender { } // ❌ AVOID - No prefix, or wrong prefix public interface UserRepository { } public interface TUserRepository { } ``` ### 1.4 Private Fields ```csharp // ✅ GOOD - Underscore prefix, camelCase public class OrderService { private readonly IOrderRepository _orderRepository; private readonly ILogger _logger; private int _retryCount; } // ❌ AVOID - No prefix, or m_ prefix private IOrderRepository orderRepository; private IOrderRepository m_orderRepository; ``` --- ## 2. Type Safety & Nullability ### 2.1 Enable Nullable Reference Types **Rule: Always enable nullable reference types in all projects** ```xml enable nullable ``` ```csharp // ✅ GOOD - Explicit nullability public string Name { get; set; } // Non-nullable: must be assigned public string? MiddleName { get; set; } // Nullable: may be null public User? FindUser(Guid id) { } // Returns null if not found public User GetUser(Guid id) { } // Never returns null (throws if not found) ``` ### 2.2 Null Handling ```csharp // ✅ GOOD - Null-coalescing and conditional operators var name = user?.Name ?? "Unknown"; var city = user?.Address?.City ?? string.Empty; user?.Notify("Welcome"); // ✅ GOOD - Null guard at method entry public void ProcessOrder(Order order) { ArgumentNullException.ThrowIfNull(order); // ... } // ✅ GOOD - Null-coalescing assignment _cache ??= new Dictionary(); // ❌ AVOID - Manual null checks where operators suffice if (user != null && user.Address != null) city = user.Address.City; ``` ### 2.3 Avoid Primitive Obsession ```csharp // ✅ GOOD - Strongly typed IDs prevent mixing up parameters public readonly record struct UserId(Guid Value); public readonly record struct OrderId(Guid Value); public Task GetOrderAsync(OrderId orderId, UserId userId) { } // ❌ AVOID - Raw Guids are easy to mix up public Task GetOrderAsync(Guid orderId, Guid userId) { } ``` --- ## 3. Async/Await Patterns ### 3.1 Always Use CancellationToken **Rule: Every public async method must accept a CancellationToken** ```csharp // ✅ GOOD - CancellationToken flows through all async calls public async Task GetUserAsync(Guid userId, CancellationToken cancellationToken = default) { var user = await _repository.FindAsync(userId, cancellationToken); return user ?? throw new NotFoundException($"User {userId} not found"); } // ❌ AVOID - No way to cancel long-running operations public async Task GetUserAsync(Guid userId) { return await _repository.FindAsync(userId); } ``` ### 10.6 When to Use Moq Instead of NSubstitute While **NSubstitute is the default**, use **Moq** for: ```csharp // Use Moq when you need MockBehavior.Strict (fail on unexpected calls) var mockRepository = new Mock(MockBehavior.Strict); mockRepository.Setup(r => r.GetOrderAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new Order()); // Use Moq for verifying complex call sequences var sequence = new MockSequence(); _serviceA.InSequence(sequence).Setup(x => x.MethodA()).ReturnsAsync(true); _serviceB.InSequence(sequence).Setup(x => x.MethodB()).ReturnsAsync(false); // Use Moq for verifying no other calls were made mock.VerifyNoOtherCalls(); // Use Moq for mocking protected members mock.Protected().Setup("GetName").Returns("test"); ``` **Note**: NSubstitute covers ~95% of real-world test scenarios. Only reach for Moq's advanced features if NSubstitute doesn't provide what you need. --- ## 4. LINQ ### 4.1 Prefer Method Syntax for Simple Chains ```csharp // ✅ GOOD - Method syntax for filter/project/sort var activeUserNames = users .Where(u => u.IsActive) .OrderBy(u => u.LastName) .Select(u => u.FullName) .ToList(); // ✅ GOOD - Query syntax for complex joins (more readable) var result = from order in orders join user in users on order.UserId equals user.Id where order.Status == OrderStatus.Pending select new { order.Id, user.Name }; ``` ### 4.2 Materialize at the Right Time ```csharp // ✅ GOOD - Materialize once, use the list var activeUsers = users.Where(u => u.IsActive).ToList(); var count = activeUsers.Count; var first = activeUsers.FirstOrDefault(); // ❌ AVOID - Multiple enumerations of IEnumerable (re-evaluates query each time) var activeUsers = users.Where(u => u.IsActive); var count = activeUsers.Count(); // evaluates query var first = activeUsers.First(); // evaluates query again ``` ### 4.3 Use Appropriate Termination Methods ```csharp // ✅ GOOD - Choose the right method for the intent var first = items.FirstOrDefault(); // null if empty var single = items.SingleOrDefault(); // null if empty, throws if >1 var any = items.Any(x => x.IsActive); // bool, stops at first match var all = items.All(x => x.IsActive); // bool, fails fast on false var count = items.Count(x => x.IsActive); // full enumeration // ❌ AVOID - Using Count() to check existence (full enumeration) if (items.Count() > 0) { } // Use Any() instead ``` ### 4.4 Avoid LINQ in Performance-Critical Paths ```csharp // ✅ GOOD - Direct loop when allocation matters var total = 0m; foreach (var item in orderItems) total += item.Price * item.Quantity; // LINQ alternative (fine for most code, avoids premature optimization) var total = orderItems.Sum(item => item.Price * item.Quantity); ``` --- ## 5. Error Handling ### 5.1 Use Specific Exception Types ```csharp // ✅ GOOD - Specific, meaningful exceptions public async Task GetUserAsync(Guid userId, CancellationToken ct = default) { var user = await _repository.FindAsync(userId, ct); if (user is null) throw new NotFoundException($"User '{userId}' was not found."); return user; } // ✅ GOOD - Domain exception hierarchy public class DomainException : Exception { public DomainException(string message) : base(message) { } public DomainException(string message, Exception inner) : base(message, inner) { } } public class NotFoundException : DomainException { public NotFoundException(string message) : base(message) { } } public class ValidationException : DomainException { public IReadOnlyList Errors { get; } public ValidationException(IReadOnlyList errors) : base("One or more validation errors occurred.") => Errors = errors; } // ❌ AVOID - Generic exceptions with no context throw new Exception("Not found"); throw new ApplicationException("Something went wrong"); ``` ### 5.2 Validate at Entry Points ```csharp // ✅ GOOD - Fail fast with guard clauses public void PlaceOrder(Order order, Guid userId) { ArgumentNullException.ThrowIfNull(order); ArgumentNullException.ThrowIfNull(userId); if (order.Items.Count == 0) throw new ValidationException(["Order must contain at least one item."]); if (order.TotalAmount <= 0) throw new ValidationException(["Order total must be greater than zero."]); // proceed with valid input } ``` ### 5.3 Result Pattern (for Expected Failures) ```csharp // ✅ GOOD - Use Result when failure is a normal outcome (not exceptional) public readonly record struct Result { public bool IsSuccess { get; } public T? Value { get; } public string? Error { get; } private Result(bool isSuccess, T? value, string? error) { IsSuccess = isSuccess; Value = value; Error = error; } public static Result Success(T value) => new(true, value, null); public static Result Failure(string error) => new(false, default, error); } // Usage public Result SubmitOrder(Cart cart) { if (!cart.HasItems) return Result.Failure("Cart is empty."); var order = CreateOrderFromCart(cart); return Result.Success(order); } var result = SubmitOrder(cart); if (result.IsSuccess) Console.WriteLine($"Order created: {result.Value!.Id}"); else Console.WriteLine($"Failed: {result.Error}"); // Reserve exceptions for truly exceptional, unexpected conditions. // Use Result for expected business failures (validation, not found in context of search, etc.) ``` ### 5.4 Catch Specific Exceptions ```csharp // ✅ GOOD - Catch what you can handle try { await _paymentGateway.ChargeAsync(amount, ct); } catch (PaymentDeclinedException ex) { _logger.LogWarning(ex, "Payment declined for amount {Amount}", amount); return Result.Failure("Payment was declined."); } catch (TimeoutException ex) { _logger.LogError(ex, "Payment gateway timed out"); throw; // re-throw: let caller or middleware handle retries } // ❌ AVOID - Swallowing exceptions silently try { await _paymentGateway.ChargeAsync(amount, ct); } catch { } // ❌ AVOID - Catching Exception broadly without re-throw catch (Exception ex) { _logger.LogError(ex, "Error"); return null; // hides the real problem } ``` --- ## 6. Pattern Matching ### 6.1 Switch Expressions (Prefer over switch statements) ```csharp // ✅ GOOD - Switch expression: concise, exhaustive public decimal GetDiscount(CustomerTier tier) => tier switch { CustomerTier.Bronze => 0.00m, CustomerTier.Silver => 0.05m, CustomerTier.Gold => 0.10m, CustomerTier.Platinum => 0.20m, _ => throw new ArgumentOutOfRangeException(nameof(tier), tier, null) }; // ❌ AVOID - Verbose switch statement for simple value mapping switch (tier) { case CustomerTier.Bronze: return 0.00m; case CustomerTier.Silver: return 0.05m; // ... } ``` ### 6.2 Type Patterns ```csharp // ✅ GOOD - Type pattern with declaration public string Describe(Shape shape) => shape switch { Circle c => $"Circle with radius {c.Radius}", Rectangle r => $"Rectangle {r.Width}x{r.Height}", Triangle t => $"Triangle with base {t.Base}", _ => "Unknown shape" }; // ✅ GOOD - is pattern for type checking with binding if (notification is EmailNotification email) { await SendEmailAsync(email.Address, email.Body, ct); } ``` ### 6.3 Property Patterns ```csharp // ✅ GOOD - Property pattern for readable conditionals public decimal CalculateShipping(Order order) => order switch { { TotalAmount: >= 100 } => 0m, // free shipping { IsExpressDelivery: true } => 15m, // express { ShippingAddress.Country: "FI" } => 5m, // domestic _ => 10m // international }; ``` ### 6.4 Deconstruction ```csharp // ✅ GOOD - Deconstruct tuples and records var (firstName, lastName) = GetFullName(userId); var (lat, lon) = location; // ✅ GOOD - Discard unused parts var (id, _, createdAt) = GetOrderSummary(orderId); ``` --- ## 7. Code Organization ### 7.1 Namespace Per Feature (not per type) ```csharp // ✅ GOOD - Feature-based namespaces namespace MyApp.Orders; // all order-related types together namespace MyApp.Users; // all user-related types together namespace MyApp.Notifications; // ❌ AVOID - Layer-based namespaces that scatter a feature across the codebase namespace MyApp.Repositories; namespace MyApp.Services; namespace MyApp.Controllers; ``` ### 7.2 File-Scoped Namespaces ```csharp // ✅ GOOD - File-scoped namespace (C# 10+): less indentation namespace MyApp.Orders; public class OrderService { } // ❌ AVOID - Block-scoped namespace adds unnecessary indentation namespace MyApp.Orders { public class OrderService { } } ``` ### 7.3 One Type Per File ```csharp // ✅ GOOD - OrderService.cs contains only OrderService // ✅ ACCEPTABLE - Small, closely related types in one file (e.g., value objects + their exceptions) // ❌ AVOID - Multiple unrelated types in one file // OrderService.cs containing OrderService + UserService + ProductRepository ``` ### 7.4 Using Directives ```csharp // ✅ GOOD - Global usings for commonly used namespaces (in a GlobalUsings.cs file) global using System; global using System.Collections.Generic; global using System.Threading; global using System.Threading.Tasks; global using Microsoft.Extensions.Logging; // ✅ GOOD - File-level usings at the top, outside namespace using System.Text.Json; using MyApp.Common; namespace MyApp.Orders; ``` ### 7.5 Class Structure Order Follow this order within a class: ```csharp public class OrderService : IOrderService { // 1. Constants private const int MaxRetryCount = 3; // 2. Static fields private static readonly JsonSerializerOptions JsonOptions = new(); // 3. Instance fields (private) private readonly IOrderRepository _repository; private readonly ILogger _logger; // 4. Constructor(s) public OrderService(IOrderRepository repository, ILogger logger) { _repository = repository; _logger = logger; } // 5. Properties // 6. Public methods // 7. Private methods } ``` --- ## 8. Records & Immutability ### 8.1 Use Records for Value Objects and DTOs ```csharp // ✅ GOOD - Record for immutable data (value semantics, built-in equality) public record UserDto(Guid Id, string Name, string Email); public record Address(string Street, string City, string PostalCode, string Country); // ✅ GOOD - Readonly record struct for small value objects (stack allocated) public readonly record struct Money(decimal Amount, string Currency); public readonly record struct Coordinates(double Latitude, double Longitude); // Non-destructive mutation via 'with' var updated = originalUser with { Email = "new@example.com" }; ``` ### 8.2 Immutable Collections ```csharp // ✅ GOOD - Expose immutable views public class Order { private readonly List _items = new(); public IReadOnlyList Items => _items.AsReadOnly(); public void AddItem(OrderItem item) { ArgumentNullException.ThrowIfNull(item); _items.Add(item); } } // ✅ GOOD - ImmutableList for truly immutable scenarios using System.Collections.Immutable; public record ShoppingCart(ImmutableList Items) { public ShoppingCart AddItem(CartItem item) => this with { Items = Items.Add(item) }; public ShoppingCart RemoveItem(Guid itemId) => this with { Items = Items.RemoveAll(i => i.Id == itemId) }; } ``` ### 8.3 init-only Properties ```csharp // ✅ GOOD - init allows construction but prevents later mutation public class OrderConfiguration { public Guid OrderId { get; init; } public string Currency { get; init; } = "EUR"; public int MaxItems { get; init; } = 100; } // Can set during object initializer, but not after var config = new OrderConfiguration { OrderId = Guid.NewGuid(), Currency = "USD" }; // config.Currency = "EUR"; // ❌ compile error ``` --- ## 9. Dependency Injection ### 9.1 Constructor Injection (Preferred) ```csharp // ✅ GOOD - All dependencies injected through constructor, stored readonly public class OrderService : IOrderService { private readonly IOrderRepository _repository; private readonly IPaymentGateway _paymentGateway; private readonly ILogger _logger; public OrderService( IOrderRepository repository, IPaymentGateway paymentGateway, ILogger logger) { _repository = repository; _paymentGateway = paymentGateway; _logger = logger; } } // ❌ AVOID - Service locator pattern (hidden dependencies) public class OrderService { public async Task ProcessAsync() { var repo = ServiceLocator.Get(); // hidden dependency } } ``` ### 9.2 Lifetime Registration ```csharp // Registration in Program.cs or an extension method services.AddScoped(); // per HTTP request services.AddTransient(); // new instance each time services.AddSingleton(); // single instance // ✅ GOOD - Extension method groups related registrations public static class OrdersServiceCollectionExtensions { public static IServiceCollection AddOrders(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); return services; } } // In Program.cs builder.Services.AddOrders(); ``` ### 9.3 Options Pattern for Configuration ```csharp // ✅ GOOD - Strongly-typed configuration public class PaymentOptions { public const string SectionName = "Payment"; public string ApiKey { get; set; } = string.Empty; public string BaseUrl { get; set; } = string.Empty; public int TimeoutSeconds { get; set; } = 30; } // Registration builder.Services.Configure( builder.Configuration.GetSection(PaymentOptions.SectionName)); // Usage public class PaymentGateway { private readonly PaymentOptions _options; public PaymentGateway(IOptions options) { _options = options.Value; } } ``` --- ## 10. Testing ### 10.1 Framework & Structure **Use xUnit** as the default test framework. Use **Shouldly** for readable assertions. Use **NSubstitute** for mocking (pragmatic, readable syntax). Use **Moq** only for scenarios requiring strict mock behavior or complex verification. ```csharp // ✅ GOOD - xUnit test class structure with NSubstitute public class OrderServiceTests { // Arrange shared fixtures in constructor or use class fixtures private readonly IOrderRepository _repositorySubstitute = Substitute.For(); private readonly ILogger _loggerSubstitute = Substitute.For>(); private readonly OrderService _sut; public OrderServiceTests() { _sut = new OrderService(_repositorySubstitute, _loggerSubstitute); } [Fact] public async Task GetOrderAsync_WhenOrderExists_ReturnsOrder() { // Arrange var orderId = Guid.NewGuid(); var expected = new Order { Id = orderId }; _repositorySubstitute .FindAsync(orderId, Arg.Any()) .Returns(Task.FromResult(expected)); // Act var result = await _sut.GetOrderAsync(orderId); // Assert result.ShouldBeEquivalentTo(expected); } [Fact] public async Task GetOrderAsync_WhenOrderNotFound_ThrowsNotFoundException() { // Arrange var orderId = Guid.NewGuid(); _repositorySubstitute .FindAsync(orderId, Arg.Any()) .Returns(Task.FromResult(null)); // Act var act = () => _sut.GetOrderAsync(orderId); // Assert await act.ShouldThrowAsync(); } [Fact] public async Task SaveOrderAsync_CallsRepository_VerifiesInteraction() { // Arrange var order = new Order { Id = Guid.NewGuid() }; // Act await _sut.SaveOrderAsync(order); // Assert await _repositorySubstitute.Received(1).SaveAsync(order, Arg.Any()); } } ``` **Why NSubstitute?** - ✅ Fluent, readable syntax — substitutes ARE the interfaces, no `.Object` indirection - ✅ Lower ceremony, less noise in tests - ✅ `Arg.Any()` is cleaner than `It.IsAny()` - ✅ `Received()` reads naturally as "verify it received this call" - ✅ Faster to write and understand, even for complex scenarios ### 10.2 Parameterized Tests ```csharp // ✅ GOOD - Theory with InlineData for multiple cases [Theory] [InlineData(CustomerTier.Bronze, 0.00)] [InlineData(CustomerTier.Silver, 0.05)] [InlineData(CustomerTier.Gold, 0.10)] [InlineData(CustomerTier.Platinum, 0.20)] public void GetDiscount_ReturnsCorrectRate(CustomerTier tier, decimal expected) { var result = _sut.GetDiscount(tier); result.ShouldBe(expected); } // ✅ GOOD - MemberData for complex input objects public static IEnumerable InvalidOrders => [ [new Order { Items = [] }, "Order must contain at least one item."], [new Order { Items = [item], TotalAmount = -1 }, "Order total must be greater than zero."], ]; [Theory] [MemberData(nameof(InvalidOrders))] public void PlaceOrder_WithInvalidOrder_ThrowsValidationException(Order order, string expectedError) { var act = () => _sut.PlaceOrder(order, Guid.NewGuid()); act.ShouldThrow() .Errors.ShouldContain(expectedError); } ``` ### 10.3 Test Naming ```csharp // ✅ GOOD - MethodName_StateUnderTest_ExpectedBehavior public async Task GetOrderAsync_WhenOrderExists_ReturnsOrder() { } public async Task GetOrderAsync_WhenOrderNotFound_ThrowsNotFoundException() { } public void PlaceOrder_WithEmptyCart_ThrowsValidationException() { } public void CalculateDiscount_ForPlatinumCustomer_Returns20Percent() { } ``` ### 10.4 Avoid Logic in Tests ```csharp // ✅ GOOD - Direct, no conditionals or loops [Fact] public void FormatName_ReturnsFullName() { var user = new User { FirstName = "John", LastName = "Doe" }; var result = _sut.FormatName(user); result.ShouldBe("John Doe"); } // ❌ AVOID - Logic in tests (makes failures hard to diagnose) [Fact] public void FormatNames_ReturnsFullNames() { var users = GetTestUsers(); foreach (var user in users) { if (user.FirstName != null) _sut.FormatName(user).ShouldContain(user.FirstName); } } ``` ### 10.5 Integration Tests ```csharp // ✅ GOOD - Use WebApplicationFactory for ASP.NET Core integration tests public class OrdersApiTests : IClassFixture> { private readonly HttpClient _client; public OrdersApiTests(WebApplicationFactory factory) { _client = factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { // Replace real DB with in-memory for tests services.RemoveAll(); services.AddDbContext(o => o.UseInMemoryDatabase("TestDb")); }); }).CreateClient(); } [Fact] public async Task GetOrder_ReturnsOk() { var response = await _client.GetAsync("/api/orders/123"); response.StatusCode.ShouldBe(HttpStatusCode.OK); } } ``` --- ## Quick Reference | Topic | Key Rule | |-------|---------| | Naming | PascalCase methods/properties, `_camelCase` fields, `I`-prefix interfaces, `Async` suffix | | Nullability | Enable `enable`, use `?` explicitly, guard with `ArgumentNullException.ThrowIfNull` | | Async | Always pass `CancellationToken`, avoid `async void`, prefer `Task.WhenAll` for parallel ops | | LINQ | Materialize with `ToList()` once, use `Any()` not `Count() > 0` | | Errors | Specific exception types, validate at entry, `Result` for expected failures | | Pattern matching | Switch expressions over switch statements, property patterns for readable conditionals | | Organization | File-scoped namespaces, feature-based folders, one type per file | | Immutability | Records for value objects/DTOs, `IReadOnlyList` for exposed collections | | DI | Constructor injection, `readonly` fields, Options pattern for config | | Testing | xUnit + Shouldly, `[Fact]`/`[Theory]`, Arrange-Act-Assert, no logic in tests |