Back to handbooks index

.NET / C# Coding Standards Handbook

Industry-standard conventions aligned with Microsoft's official guidelines, Google's C# style guide, and production patterns for .NET 8 / 9.

.NET 8 / 9 C# 12 / 13 ASP.NET Core Microsoft + Google style

🏷 Naming Conventions

Consistent naming is the single highest-leverage standard. Follow Microsoft's official .NET naming guidelines — they're the baseline every employer, reviewer, and linter expects.

IdentifierConventionExampleNotes
Namespace PascalCase MyApp.Services.Auth Company.Product.Feature
Class / Record / Struct PascalCase UserAccount Nouns preferred
Interface I + PascalCase IOrderRepository Always prefix I
Method / Property PascalCase GetUserById, IsActive Verbs for methods
Local variable / param camelCase userId, orderCount No abbreviations
Private field _camelCase _messageWriter Underscore prefix (Google/MS runtime style)
Private static field s_camelCase s_defaultTimeout s_ prefix (MS runtime style)
Constant PascalCase MaxRetryCount NOT SCREAMING_CAPS
Enum type PascalCase (singular) OrderStatus Flags enum → plural
Enum member PascalCase OrderStatus.Pending No numeric suffix
Event PascalCase (verb phrase) OrderPlaced, DataLoaded Past tense preferred
Async method PascalCase + Async suffix GetUserAsync Required when sync counterpart exists
Generic type param T + PascalCase TEntity, TResult T alone if single param
Boolean variable is/has/can + camelCase isActive, hasPermission Question-form naming
Never use Hungarian notation. No strName, iCount, objUser. The IDE shows types in tooltips. Abbreviations obscure intent; prefer employeeAssignment over empAsgmt. Exceptions: well-known acronyms (Id, Xml, Http, Uri, Ftp).
✓ DO
public class OrderService
{
    private readonly IOrderRepository _orderRepo;
    private static readonly TimeSpan s_defaultTimeout
        = TimeSpan.FromSeconds(30);

    public const int MaxRetryCount = 3;

    public async Task<Order> GetOrderAsync(
        int orderId,
        CancellationToken ct = default)
    {
        var order = await _orderRepo
            .FindByIdAsync(orderId, ct);
        return order;
    }
}
✕ DON'T
public class orderSvc
{
    private IOrderRepository OrderRepo; // public-style name
    private static TimeSpan DEFAULT_TIMEOUT
        = TimeSpan.FromSeconds(30); // screaming caps

    public const int MAX_RETRY_CNT = 3; // abbr + CAPS

    public async Task<Order> getOrder(
        int intOrderId) // Hungarian + no CT
    {
        var obj = await OrderRepo
            .FindByIdAsync(intOrderId);
        return obj; // meaningless name
    }
}

📐 File & Code Layout

MUST
One type per file
Each class, record, interface, or enum lives in its own file. Filename matches type name exactly: OrderService.cs.
MUST
File-scoped namespaces
Use namespace MyApp.Services; (no braces). Reduces one indentation level across the entire file.
MUST
One statement per line
Never chain statements on one line. One declaration per line. Max line length: 120 characters.
SHOULD
Using directive order
System namespaces first, then third-party, then project-internal. Alphabetically within each group. No unused usings.
SHOULD
Member ordering
Fields → Constructors → Properties → Methods → Nested types. Public members before private within each group.
SHOULD
Seal classes by default
Add sealed to classes not designed for inheritance. Improves JIT devirtualization and signals design intent.
// 1. System usings first
using System.Text.Json;

// 2. Third-party
using Microsoft.Extensions.Logging;
using Serilog;

// 3. Internal project
using MyApp.Domain.Entities;
using MyApp.Infrastructure.Persistence;

// 4. File-scoped namespace (no braces)
namespace MyApp.Services;

// 5. Type with member ordering
public sealed class UserService(IUserRepository repo, ILogger<UserService> logger)
{
    // Fields
    private readonly IUserRepository _repo = repo;
    private readonly ILogger<UserService> _logger = logger;

    // Properties
    public bool IsInitialized { get; private set; }

    // Methods — public before private
    public async Task<User?> GetByIdAsync(int id, CancellationToken ct)
    {
        _logger.LogInformation("Fetching user {UserId}", id);
        return await _repo.FindAsync(id, ct);
    }

    private static bool IsValidId(int id) => id > 0;
}

🧩 Type Design

Prefer immutability — use records

// Positional record — immutable value type, structural equality, deconstruct built-in
public sealed record Money(decimal Amount, string Currency);

// With required init-only properties for DTOs
public sealed class CreateUserRequest
{
    public required string Name  { get; init; }
    public required string Email { get; init; }
}

// Primary constructor (C# 12) — inject dependencies concisely
public sealed class OrderHandler(IOrderRepo repo, ILogger<OrderHandler> log)
{
    public async Task HandleAsync(Order order) { /* ... */ }
}

Enum design

// Singular enum name, PascalCase members
public enum OrderStatus { Pending, Processing, Completed, Cancelled }

// [Flags] enum → plural name + explicit powers-of-2 values
[Flags]
public enum Permissions
{
    None   = 0,
    Read   = 1,
    Write  = 2,
    Delete = 4
}

Avoid: anemic domain models, god classes, static everything

Static is global state. Avoid static mutable fields. Static classes are appropriate for pure utility methods (e.g., Guard, StringHelper) but not for services holding state or orchestrating logic. Every static dependency is a hidden coupling that breaks unit testing.

Modern C# Features

Always use the highest C# version available in the project. Enable <LangVersion>latest</LangVersion> in the .csproj. Prefer modern constructs — they reduce noise and surface bugs earlier.

Pattern matching

// Switch expression + property patterns
var discount = order switch
{
    { Status: OrderStatus.Completed, Total: > 1000m } => 0.10m,
    { Customer.IsPremium: true }                       => 0.05m,
    _                                                    => 0m
};

// List patterns (C# 11)
if (items is [var first, .., var last])
    Process(first, last);

Collection expressions & spread (C# 12)

// Concise collection creation
int[] evens = [2, 4, 6, 8];
List<string> combined = [..defaults, ..overrides];
ReadOnlySpan<byte> magic = [0x89, 0x50, 0x4E, 0x47];

Raw string literals, interpolated strings

// Raw string for SQL, JSON, regex — no escaping nightmares
var sql = """
    SELECT u.Id, u.Name
    FROM   Users u
    WHERE  u.IsActive = 1
      AND  u.CreatedAt > @cutoff
    """;

// Prefer interpolated strings over string.Format
_logger.LogInformation($"Order {orderId} placed at {DateTimeOffset.UtcNow:O}");
// ⚠ But use structured logging params in ILogger — not string interpolation
_logger.LogInformation("Order {OrderId} placed", orderId); // ✓

var usage

Use var when the type is obvious from the right-hand side. Never use var when it obscures the type (e.g., numeric literals, return values of opaque methods).

✓ DO — type is obvious
var user    = new User();
var orders  = await _repo.GetAllAsync();
var message = string.Empty;
✕ DON'T — type is ambiguous
var x = GetSomething();   // what type?
var n = 100;               // int? long? double?
var flag = Compute(data); // bool? int?

🔒 Nullable Reference Types

Enable #nullable enable project-wide (set in .csproj). This is design-time and compile-time safety — it does not add runtime null checks, but it eliminates an entire class of bugs by surfacing them at build time.

<PropertyGroup>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
✓ DO
// Non-null — compiler enforces initialization
public string Name { get; init; }

// Nullable — explicit opt-in
public string? MiddleName { get; init; }

// Null guard at boundaries
public User Create(string? name)
{
    ArgumentNullException.ThrowIfNull(name);
    return new User(name);
}
✕ DON'T
// Never use null-forgiving ! unless unavoidable
var name = user.Name!; // code smell

// Never disable nullability warnings
#pragma warning disable CS8602
var len = user.Name.Length; // hidden NPE
#pragma warning restore CS8602
Migration strategy: For brownfield projects, enable <Nullable>annotations</Nullable> first (adds metadata, no warnings), then graduate to enable file-by-file. Use #nullable enable at the top of individual files to migrate incrementally.

🔗 LINQ

Prefer LINQ method syntax over query syntax for most use cases. Avoid LINQ in hot paths — prefer foreach or Span<T> when allocations matter.

✓ DO — method syntax, meaningful names
var activeAdmins = users
    .Where(u => u.IsActive && u.Role == Role.Admin)
    .OrderBy(u => u.LastName)
    .Select(u => u.Email)
    .ToList();

// IQueryable deferred execution — filter in DB
var query = _db.Orders
    .Where(o => o.CustomerId == customerId)
    .AsNoTracking();

if (status.HasValue)
    query = query.Where(o => o.Status == status);

var results = await query.ToListAsync(ct);
✕ DON'T
// Don't load all data into memory then filter
var bad = await _db.Orders.ToListAsync();
var filtered = bad.Where(o => o.IsActive); // N+1

// Avoid deeply chained LINQ with side effects
users.ToList().ForEach(u => Send(u)); // use foreach

// Don't use Count() when Any() suffices
if (list.Count() > 0) // ❌
if (list.Any())       // ✓
💡
IQueryable vs IEnumerable: Keep the chain as IQueryable until you call ToListAsync()/FirstOrDefaultAsync(). Materializing early with ToList() before filtering runs the query in memory — a common EF Core performance trap.

⚙️ Async / Await

All I/O-bound operations must be async. Never block async code synchronously — it causes deadlocks in ASP.NET Core.

RuleSeverityReason
Always return Task/Task<T>, never void (except event handlers) MUST Void async swallows exceptions silently
Suffix async methods with Async MUST Required when sync counterpart exists; convention everywhere else
Pass CancellationToken through the call chain MUST Enables cooperative cancellation on request teardown
Never use .Result, .Wait(), GetAwaiter().GetResult() MUST Deadlocks in synchronization-context environments
Use ConfigureAwait(false) in library/infrastructure code SHOULD Avoids unnecessary sync-context capture in non-UI layers
Prefer ValueTask<T> for high-frequency hot paths SHOULD Avoids Task allocation when result is synchronous
Never await in a constructor MUST Use factory pattern or IHostedService.StartAsync
Use await using for IAsyncDisposable MUST Ensures async cleanup (DB connections, streams)
✓ DO — async all the way down
public async Task<Order?> GetOrderAsync(
    int orderId,
    CancellationToken ct = default)
{
    await using var conn = await _pool
        .OpenAsync(ct).ConfigureAwait(false);

    return await conn
        .QueryFirstOrDefaultAsync<Order>(sql, new { orderId })
        .ConfigureAwait(false);
}

// Async factory pattern — when async init is needed
public static async Task<DataLoader> CreateAsync(
    CancellationToken ct)
{
    var data = await FetchInitialDataAsync(ct);
    return new DataLoader(data);
}
✕ DON'T — sync-over-async
// DEADLOCK — blocks thread waiting for async
public Order? GetOrder(int id)
{
    return GetOrderAsync(id).Result; // ❌
}

// Swallows exceptions
public async void ProcessOrder() { // ❌
    await _service.DoWorkAsync();
}

// Await in constructor
public MyService(IRepo repo)
{
    _data = LoadAsync().Result; // ❌ deadlock
}

Parallel async

// Run independent tasks concurrently
var (users, products) = await (GetUsersAsync(ct), GetProductsAsync(ct));

// Bounded parallel I/O — avoid task flooding
await Parallel.ForEachAsync(
    orders,
    new ParallelOptions { MaxDegreeOfParallelism = 4, CancellationToken = ct },
    async (order, innerCt) => await ProcessAsync(order, innerCt));

🏗 SOLID Principles

S — SRP
Single Responsibility
A class has one reason to change. If you need "and" to describe what it does, split it. Target: methods < 20 lines, classes < 200 lines.
O — OCP
Open / Closed
Open for extension, closed for modification. Use abstract base classes, interfaces, or strategy pattern to add behavior without editing existing code.
L — LSP
Liskov Substitution
Subtypes must be substitutable for their base type without altering correctness. If you're throwing NotImplementedException in an override, LSP is violated.
I — ISP
Interface Segregation
Many small interfaces beat one fat interface. IReadableRepository<T> + IWritableRepository<T> instead of a single IRepository<T> with 15 methods.
D — DIP
Dependency Inversion
High-level modules depend on abstractions (interfaces), not concretions. The DI container injects implementations at runtime. Never new a service inside another service.

💉 Dependency Injection

LifetimePerUse forRisk
Singleton Application Config, caches, thread-safe stateless services, HttpClient factories Capturing Scoped deps → scope leak
Scoped HTTP request / DI scope DbContext, Unit of Work, repositories — default for most services Captured in Singleton → stale state
Transient Every resolution Lightweight, stateless utilities Too many = GC pressure
// Register services — prefer extension methods per layer
builder.Services
    .AddScoped<IOrderService, OrderService>()
    .AddScoped<IOrderRepository, OrderRepository>()
    .AddSingleton<ICacheService, RedisCacheService>()
    .AddHttpClient<IPaymentGateway, StripeGateway>(c =>
        c.BaseAddress = new Uri(config["Stripe:BaseUrl"]!));

// ✓ Constructor injection — primary pattern
public sealed class OrderService(
    IOrderRepository repository,
    ILogger<OrderService> logger)
{
    public async Task<Order> PlaceOrderAsync(
        PlaceOrderCommand cmd, CancellationToken ct)
    {
        // ...
    }
}

// ✓ Scoped operation in a Singleton via IServiceScopeFactory
public sealed class BackgroundWorker(IServiceScopeFactory scopeFactory)
{
    public async Task DoWorkAsync(CancellationToken ct)
    {
        await using var scope = scopeFactory.CreateAsyncScope();
        var repo = scope.ServiceProvider
            .GetRequiredService<IOrderRepository>();
        // ...
    }
}
Avoid service locator. Never inject IServiceProvider just to call GetService<T>() in application code — that's the service locator anti-pattern. It hides dependencies and makes code untestable. Only acceptable at composition root boundaries (e.g., middleware, background workers needing scoped services).

🚨 Error Handling

MUST
Only catch what you can handle
Never catch Exception silently. Use specific exception types. If you can't recover, let it propagate.
MUST
Never swallow exceptions
Empty catch { } blocks are forbidden. At minimum, log and rethrow. Use throw; not throw ex; to preserve stack trace.
SHOULD
Use global exception handler
In ASP.NET Core, implement IExceptionHandler for ProblemDetails responses. Never let raw exceptions reach the client.
SHOULD
Guard at boundaries
Use ArgumentNullException.ThrowIfNull, ArgumentOutOfRangeException.ThrowIfNegative at public method entry points.
// ✓ Specific catch, meaningful message
try
{
    return await _repo.GetAsync(id, ct);
}
catch (EntityNotFoundException ex)
{
    _logger.LogWarning(ex, "Order {OrderId} not found", id);
    return null;
}

// ✓ Global exception handler (ASP.NET Core)
public sealed class GlobalExceptionHandler(
    ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext ctx, Exception ex, CancellationToken ct)
    {
        var traceId = Activity.Current?.Id ?? ctx.TraceIdentifier;
        logger.LogError(ex,
            "Unhandled exception on {Machine} trace={TraceId}",
            Environment.MachineName, traceId);

        (int status, string title) = ex switch
        {
            ValidationException  => (400, "Bad Request"),
            UnauthorizedException => (401, "Unauthorized"),
            NotFoundException    => (404, "Not Found"),
            _                     => (500, "Internal Server Error")
        };

        await ctx.Response.WriteAsJsonAsync(
            new ProblemDetails { Status = status, Title = title,
                Extensions = { ["traceId"] = traceId } }, ct);
        return true;
    }
}

📋 Logging

✓ DO — structured, source-generated
// Structured logging — searchable in Seq/Loki/Splunk
logger.LogInformation(
    "Order {OrderId} placed for customer {CustomerId}",
    orderId, customerId);

// Source-generated log methods (fastest, zero alloc)
public static partial class Log
{
    [LoggerMessage(
        Level = LogLevel.Information,
        Message = "Order {OrderId} placed")]
    public static partial void OrderPlaced(
        this ILogger logger, int orderId);
}

// Conditional to avoid string allocation
if (logger.IsEnabled(LogLevel.Debug))
    logger.LogDebug("Expensive detail: {Data}", BuildPayload());
✕ DON'T
// Interpolated string — loses structured data
logger.LogInformation(
    $"Order {orderId} placed"); // ❌ no structure

// Logging exceptions incorrectly
catch (Exception ex)
{
    logger.LogError($"Error: {ex.Message}"); // ❌
    // ✓ logger.LogError(ex, "Failed to process");
}

// Console.WriteLine in production
Console.WriteLine("Something happened"); // ❌
LevelUse when
TraceExtremely verbose — loop internals, method entry/exit. Dev only.
DebugDiagnostic info useful during development and troubleshooting.
InformationNormal application events — request received, order placed, user logged in.
WarningUnexpected but recoverable — retry attempt, degraded mode, config fallback.
ErrorFailures requiring attention — exception caught, operation failed. Pass the exception object.
CriticalSystem-level failure — data corruption, process crash, unrecoverable state.

🛡 Security Standards

MUST
Never hard-code secrets
API keys, passwords, connection strings → environment variables, Azure Key Vault, or Secret Manager (dev). Never in source code or appsettings.json committed to git.
MUST
Parameterized queries always
Use EF Core, Dapper with parameters, or ADO.NET SqlParameter. Never concatenate user input into SQL strings — SQL injection.
MUST
Validate all inputs
Use FluentValidation or DataAnnotations at the boundary. Validate before processing. Reject and return 400, not sanitize-and-continue.
MUST
HTTPS everywhere
Force HTTPS redirect and HSTS. Use UseHttpsRedirection() + UseHsts() in Program.cs.
SHOULD
Use Data Protection API
For encrypting sensitive cookies, tokens, or persisted data. Never roll your own crypto.
SHOULD
Apply least privilege
Minimal DB permissions per service. Use connection strings with read-only access for read services. Service accounts, not admin accounts.
// ✓ Parameterized query with Dapper
var user = await conn.QueryFirstOrDefaultAsync<User>(
    "SELECT * FROM Users WHERE Email = @Email",
    new { Email = email });

// ✓ Secrets from configuration
var connStr = builder.Configuration["Database:ConnectionString"]
    ?? throw new InvalidOperationException("DB connection string missing");

// ✓ Hashing passwords — never MD5/SHA1
var hash = BCrypt.Net.BCrypt.HashPassword(plainPassword, workFactor: 12);
bool valid = BCrypt.Net.BCrypt.Verify(plainPassword, storedHash);

🚀 Performance

✓ DO — allocation-aware
// Span<T> for stack-allocated slices
ReadOnlySpan<char> slice = input.AsSpan(0, 10);

// StringBuilder for string-heavy loops
var sb = new StringBuilder();
foreach (var item in items) sb.Append(item).Append(',');

// ArrayPool to avoid large heap allocations
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try { /* use buffer */ }
finally { ArrayPool<byte>.Shared.Return(buffer); }

// Frozen collections for read-only lookup (net8)
private static readonly FrozenSet<string> s_keywords
    = new[] { "buy", "sell", "hold" }.ToFrozenSet();
✕ DON'T — hot-path pitfalls
// String concat in loop — O(n²) allocs
string result = "";
foreach (var item in items)
    result += item; // ❌

// LINQ on hot path — enumerator allocs
while (true)
{
    var sum = list.Sum(); // ❌ in tight loop
}

// Boxing value types unnecessarily
ArrayList list = new(); // ❌ boxes int, bool
List<int> typed = new(); // ✓
💡
Measure before optimizing. Use BenchmarkDotNet to profile hot paths. Use dotMemory or dotnet-trace to find GC pressure. Never optimize speculatively — always benchmark with realistic data volumes.

🧪 Testing Standards

Tests are first-class citizens. Untestable code is a design smell. Target: ≥80% line coverage on business logic; 100% on critical paths.

Naming: Arrange / Act / Assert + Given/When/Then

public class OrderServiceTests
{
    private readonly IOrderRepository _repo
        = Substitute.For<IOrderRepository>();
    private readonly ILogger<OrderService> _logger
        = Substitute.For<ILogger<OrderService>>();

    [Fact]
    public async Task
        PlaceOrderAsync_GivenValidCommand_ShouldReturnCreatedOrder()
    {
        // Arrange
        var cmd = new PlaceOrderCommand(CustomerId: 1, Sku: "ABC");
        _repo.SaveAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>())
             .Returns(new Order(Id: 42, Sku: "ABC"));
        var sut = new OrderService(_repo, _logger);

        // Act
        var result = await sut.PlaceOrderAsync(cmd, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result!.Id.Should().Be(42);
        await _repo.Received(1).SaveAsync(
            Arg.Any<Order>(), Arg.Any<CancellationToken>());
    }
}
LayerFrameworkNotes
Unit testsxUnit + NSubstitute + FluentAssertionsFast, isolated, mock all I/O
Integration testsxUnit + WebApplicationFactory + TestcontainersReal DB (Postgres/SQL in Docker), real HTTP
Architecture testsNetArchTest or ArchUnitNETEnforce layer dependencies as tests
Mutation testingStryker.NETFind tests that pass without catching bugs
Load testingk6 / NBomberValidate throughput under realistic concurrency
Testing anti-patterns to avoid: Tests with magic numbers (use named constants), tests that share mutable state, integration tests that depend on external services without mocking, and test methods that test more than one behaviour (one assertion concept per test).

📖 Documentation

/// <summary>
/// Retrieves an order by its unique identifier.
/// Returns <see langword="null"/> if no order exists with the given ID.
/// </summary>
/// <param name="orderId">The unique identifier of the order.</param>
/// <param name="ct">Cancellation token for the async operation.</param>
/// <returns>The matching <see cref="Order"/>, or <see langword="null"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">
///   Thrown when <paramref name="orderId"/> is less than or equal to zero.
/// </exception>
public async Task<Order?> GetOrderAsync(int orderId, CancellationToken ct);

🔧 Tooling & Static Analysis

🔍
Roslyn Analyzers (built-in)
Enabled by default in .NET SDK. Set <AnalysisLevel>latest</AnalysisLevel> and <TreatWarningsAsErrors>true</TreatWarningsAsErrors> in release builds.
Included in .NET SDK
🧹
StyleCop.Analyzers
Enforces C# style rules (spacing, member ordering, doc comments). Configured via .editorconfig. Runs at build time, no IDE required.
StyleCop.Analyzers (NuGet)
Roslynator
500+ analyzers, refactorings, and code fixes. Lightweight complement to StyleCop. Catches redundant code, missed null checks, simplifiable expressions.
Roslynator.Analyzers (NuGet)
🛡
SonarQube / SonarCloud
Deep static analysis, security hotspot detection, code smell tracking, technical debt quantification. Integrates into CI pipeline via SonarScanner.
SonarScanner.MSBuild (NuGet) + SonarCloud CI
✂️
.editorconfig + dotnet format
Consistent formatting enforced at save (IDE) and in CI (dotnet format --verify-no-changes). Covers indentation, bracket style, using placement, newlines.
Built into .NET SDK (dotnet format)
📊
BenchmarkDotNet
Micro-benchmarking with statistical rigor. Use for validating performance of hot paths. Prevents performance regression in CI via baseline comparison.
BenchmarkDotNet (NuGet)

Recommended .editorconfig snippet

[*.cs]
indent_style              = space
indent_size               = 4
end_of_line               = lf
charset                   = utf-8-bom
trim_trailing_whitespace  = true
insert_final_newline      = true
max_line_length           = 120

# Naming rules
dotnet_naming_rule.private_fields.symbols              = private_fields
dotnet_naming_rule.private_fields.style                = prefix_underscore
dotnet_naming_symbols.private_fields.applicable_kinds  = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.prefix_underscore.capitalization   = camel_case
dotnet_naming_style.prefix_underscore.required_prefix  = _

# Language preferences
csharp_prefer_simple_using_statement                   = true:suggestion
csharp_style_namespace_declarations                    = file_scoped:warning
csharp_style_prefer_primary_constructors               = true:suggestion
csharp_style_expression_bodied_methods                 = when_on_single_line

# Nullability
dotnet_diagnostic.CS8600.severity = error   # Converting null to non-nullable
dotnet_diagnostic.CS8602.severity = error   # Dereference of possibly null
dotnet_diagnostic.CS8604.severity = error   # Null argument for non-nullable param

CI/CD gates (enforce, don't just report)

# Enforce in CI — fail build on violation
dotnet build --configuration Release /warnaserror
dotnet format --verify-no-changes
dotnet test --collect:"XPlat Code Coverage" --threshold 80
dotnet tool run dotnet-stryker  # mutation score gate

🔗 Reference Links

Quick-scan rule severity matrix

AreaMustShouldAvoid
NamingPascalCase classes/methods, camelCase locals, _camelCase privates, I-prefix interfacesAbbreviation-free names, Boolean is/has prefixHungarian notation, SCREAMING_CAPS constants, single-letter names outside loops
AsyncNo .Result/.Wait(), always pass CancellationToken, never async void (except events)ConfigureAwait(false) in libs, ValueTask on hot pathsAsync in constructors, fire-and-forget without error handling
NullabilityEnable project-wide, ArgumentNullException.ThrowIfNull at boundariesAvoid nullable reference types in domain models where possibleNull-forgiving ! operator except where absolutely unavoidable
ExceptionsSpecific catch types, never swallow, always log with exception objectGlobal exception handler for APIs returning ProblemDetailsCatch(Exception) with empty body, throw ex (loses stack trace)
DIConstructor injection, respect lifetimes, never new a serviceIServiceScopeFactory for Singleton→Scoped, register per layerService locator pattern, property injection in ASP.NET Core
SecurityNo hardcoded secrets, parameterized queries, validate all inputsbcrypt/Argon2 for passwords, Data Protection APIMD5/SHA1 passwords, string-concat SQL, trusting user-controlled data
TestingArrange/Act/Assert structure, one concept per test, mock all I/O≥80% line coverage on business logic, integration tests with TestcontainersShared mutable test state, testing implementation details