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
Purpose: Universal C# best practices for AI agents working on .NET projects
Scope: Language-level patterns, not framework-specific
Last Updated: 2026-03-15
| 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 |
// ✅ 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) { }
// ✅ 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 { }
// ✅ 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;
Rule: Always enable nullable reference types in all projects
<!-- .csproj -->
<PropertyGroup>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
// ✅ 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)
// ✅ 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;
// ✅ 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) { }
Rule: Every public async method must accept a CancellationToken
// ✅ 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);
}
While NSubstitute is the default, use Moq for:
// 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.
// ✅ 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 };
// ✅ 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
// ✅ 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
// ✅ 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);
// ✅ 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");
// ✅ 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
}
// ✅ 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.)
// ✅ 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
}
// ✅ 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;
// ...
}
// ✅ 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);
}
// ✅ 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
};
// ✅ GOOD - Deconstruct tuples and records
var (firstName, lastName) = GetFullName(userId);
var (lat, lon) = location;
// ✅ GOOD - Discard unused parts
var (id, _, createdAt) = GetOrderSummary(orderId);
// ✅ 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;
// ✅ 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 { }
}
// ✅ 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
// ✅ 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;
Follow this order within a class:
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
}
// ✅ 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" };
// ✅ 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) };
}
// ✅ 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
// ✅ 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
}
}
// 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();
// ✅ 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;
}
}
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.
// ✅ 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?
.Object indirectionArg.Any<T>() is cleaner than It.IsAny<T>()Received() reads naturally as "verify it received this call"// ✅ 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);
}
// ✅ 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() { }
// ✅ 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);
}
}
// ✅ 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);
}
}
| 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 |