1
0
Fork 0
mirror of https://github.com/pnx/dotfiles synced 2026-11-01 19:56:46 +01:00

update opencode

This commit is contained in:
Henrik Hautakoski 2026-09-13 20:12:22 +02:00
parent c7cf88af98
commit 38989f191a
9 changed files with 2240 additions and 14 deletions

View file

@ -0,0 +1,903 @@
---
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
---
<!-- Context: core/standards | Priority: critical | Version: 1.1 | Updated: 2026-03-15 -->
# 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<User> GetUserByIdAsync(Guid userId) { }
public async Task<IReadOnlyList<Order>> 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<User> fetchuser(Guid id) { }
public async Task<User> 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<OrderService> _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
<!-- .csproj -->
<PropertyGroup>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
```
```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<string, User>();
// ❌ 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<Order> GetOrderAsync(OrderId orderId, UserId userId) { }
// ❌ AVOID - Raw Guids are easy to mix up
public Task<Order> 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<User> 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<User> 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<IOrderRepository>(MockBehavior.Strict);
mockRepository.Setup(r => r.GetOrderAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.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<string>("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<User> 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<string> Errors { get; }
public ValidationException(IReadOnlyList<string> 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<T> when failure is a normal outcome (not exceptional)
public readonly record struct Result<T>
{
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<T> Success(T value) => new(true, value, null);
public static Result<T> Failure(string error) => new(false, default, error);
}
// Usage
public Result<Order> SubmitOrder(Cart cart)
{
if (!cart.HasItems)
return Result<Order>.Failure("Cart is empty.");
var order = CreateOrderFromCart(cart);
return Result<Order>.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<T> 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<Receipt>.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<OrderService> _logger;
// 4. Constructor(s)
public OrderService(IOrderRepository repository, ILogger<OrderService> 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<OrderItem> _items = new();
public IReadOnlyList<OrderItem> 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<CartItem> 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<OrderService> _logger;
public OrderService(
IOrderRepository repository,
IPaymentGateway paymentGateway,
ILogger<OrderService> logger)
{
_repository = repository;
_paymentGateway = paymentGateway;
_logger = logger;
}
}
// ❌ AVOID - Service locator pattern (hidden dependencies)
public class OrderService
{
public async Task ProcessAsync()
{
var repo = ServiceLocator.Get<IOrderRepository>(); // hidden dependency
}
}
```
### 9.2 Lifetime Registration
```csharp
// Registration in Program.cs or an extension method
services.AddScoped<IOrderService, OrderService>(); // per HTTP request
services.AddTransient<IEmailSender, SmtpEmailSender>(); // new instance each time
services.AddSingleton<ICacheService, MemoryCacheService>(); // single instance
// ✅ GOOD - Extension method groups related registrations
public static class OrdersServiceCollectionExtensions
{
public static IServiceCollection AddOrders(this IServiceCollection services)
{
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IOrderRepository, OrderRepository>();
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<PaymentOptions>(
builder.Configuration.GetSection(PaymentOptions.SectionName));
// Usage
public class PaymentGateway
{
private readonly PaymentOptions _options;
public PaymentGateway(IOptions<PaymentOptions> 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<IOrderRepository>();
private readonly ILogger<OrderService> _loggerSubstitute = Substitute.For<ILogger<OrderService>>();
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<CancellationToken>())
.Returns(Task.FromResult<Order?>(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<CancellationToken>())
.Returns(Task.FromResult<Order?>(null));
// Act
var act = () => _sut.GetOrderAsync(orderId);
// Assert
await act.ShouldThrowAsync<NotFoundException>();
}
[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<CancellationToken>());
}
}
```
**Why NSubstitute?**
- ✅ Fluent, readable syntax — substitutes ARE the interfaces, no `.Object` indirection
- ✅ Lower ceremony, less noise in tests
- ✅ `Arg.Any<T>()` is cleaner than `It.IsAny<T>()`
- ✅ `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<object[]> 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<ValidationException>()
.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<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public OrdersApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace real DB with in-memory for tests
services.RemoveAll<DbContext>();
services.AddDbContext<AppDbContext>(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 `<Nullable>enable</Nullable>`, 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<T>` 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 |