13 AI Coding Conventions for Production-Ready C#/.NET Code

    AI-generated C#/.NET code can compile, run locally, and still fail in production because it violates important runtime, concurrency, observability, and security expectations. This article explains how teams can convert their engineering standards into a project-level conventions file, such as AGENTS.md, and presents 13 practical rules for working with AI coding assistants in .NET projects. The rules cover code style and contracts, async and concurrency behavior, configuration and exception handling, logging and data access, modern language features, and security guardrails. The goal is to make the guidance verifiable, executable, and easy to apply, so both developers and AI assistants can produce code that is closer to production-ready.

    Why AI-Generated C#/.NET Code Can Compile but Still Be Unsafe to Ship

    When an AI coding assistant is asked to “add an endpoint that returns paginated orders for a customer,” the generated code usually compiles, starts correctly, and works fine against a small local dataset. Problems appear later, especially in pre-production or load-testing environments: thread pool starvation caused by synchronous blocking, database queries that continue running after a client disconnects, or an external dependency timeout that brings down the whole service.

    These defects have a common trait: the compiler cannot detect them, and they are difficult to reproduce on a developer machine. One reason is that model training data contains a large amount of tutorials and historical code, including many outdated patterns. Between “compiles successfully” and “safe to deploy,” there is a large amount of team-specific knowledge: nullability conventions, resource lifetime rules, cancellation propagation, error-handling strategy, and operational expectations. Most of that knowledge exists only in senior engineers’ minds or in code review comments, and has never been written down in a form that both people and AI tools can reliably follow.

    A practical response is to turn the team’s engineering standards into a structured, project-level conventions file, for example AGENTS.md at the repository root. The file serves two purposes: it gives AI coding assistants project-specific instructions, and it defines a shared “team specification” that both humans and AI should follow. It should be maintained by the team and evolve with the project.

    The conventions file should follow three principles:

    • Verifiable: Every rule should map to a compiler warning, static analyzer rule, automated test, or other checkable outcome. Vague statements such as “code should be elegant” are not useful.
    • Executable: Short positive and negative examples are usually more effective than abstract descriptions. Show what not to write and what to write instead.
    • Tailorable: Keep only the rules that matter for the project. Large systems can retain all rules; small utilities can keep only the most critical ones. Too many rules dilute attention and reduce compliance.

    The 13 rules below are grouped into six areas: code style and contracts, async and concurrency, configuration and exceptions, logging and data access, modern language features, and security guardrails.

    1. Code Style and Contracts: Let the Compiler Enforce the Basics

    Rule 1: Use file-scoped namespaces and enable nullable reference types

    Traditional block-scoped namespaces add an extra indentation level to every file. Some models still generate this style by default. File-scoped namespaces keep type declarations at the top level of the file and make structure easier to read. They also make it easier for analyzers to apply namespace-specific rules, such as enforcing API conventions only for types in a .Api namespace.

    Nullable reference types, NRTs, are even more important. Without them, models often generate ambiguous string parameters. From the name alone, you cannot tell whether the value can be null. Downstream code then adds defensive ?. operators everywhere, and a NullReferenceException eventually appears on some edge-case input. Customer? means “may be null,” while Customer means “never null.” That distinction is valuable API documentation, and the compiler can enforce it.

    1. <!-- Example .csproj settings -->
    2. <PropertyGroup>
    3. <TargetFramework>net8.0</TargetFramework>
    4. <Nullable>enable</Nullable>
    5. <LangVersion>latest</LangVersion>
    6. <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    7. <AnalysisLevel>latest-recommended</AnalysisLevel>
    8. <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    9. </PropertyGroup>

    The conventions file should state explicitly that developers and AI assistants must not use the null-forgiving operator ! to silence warnings. If a value can legitimately be null, handle it with a null check and throw an exception with a clear message when the value is required. This combination provides several benefits:

    • Nullability warnings become compile-time errors, so null-reference risks are exposed early.
    • Recommended analyzer rules are enabled.
    • IDE code-style rules are enforced during the build.
    • The language version is kept current.

    <AnalysisLevel>latest-recommended</AnalysisLevel> enables a set of recommended analyzer rules. Several are particularly relevant for AI-generated code:

    • CA1062: Validate arguments of public methods.
    • CA1849: Call async APIs from async methods when possible.
    • CA2016: Forward a CancellationToken to APIs that support cancellation.
    • CA1310: Specify StringComparison explicitly for string comparisons.

    These rules target common mistakes that AI assistants frequently make.

    Rule 2: Use record for DTOs, domain events, and value types

    If a type is primarily data, equality, and immutability, it should be declared as a record or record struct. Models often generate mutable classes by default, which usually require hand-written Equals, GetHashCode, and ToString implementations. After properties are added or removed, those hand-written members are often forgotten.

    1. // Transport object: value equality, immutable, and print-friendly
    2. public sealed record OrderDto(
    3. Guid Id,
    4. Guid CustomerId,
    5. decimal Total,
    6. IReadOnlyList<OrderLineDto> Lines);
    7. // Domain event: carries occurrence time and aggregate identity
    8. public sealed record OrderPaid(
    9. Guid OrderId,
    10. decimal Amount,
    11. DateTimeOffset PaidAt);
    12. // Small immutable value: record struct avoids heap allocation
    13. public readonly record struct Money(decimal Amount, string Currency);

    Record types provide three important benefits:

    • with expressions can create a modified copy without mutating the original object, for example order with { Status = OrderStatus.Paid }. This fits functional-style state transitions well.
    • Value-based equality makes unit-test assertions simpler and makes it easier to use objects as dictionary keys or in sets for deduplication.
    • Primary constructor parameters are automatically exposed as read-only properties, reducing a class of bugs where objects are accidentally modified after construction.

    There is an important boundary. Entities with identity and behavior, such as aggregate roots used with an ORM, should still be classes. Entity equality is usually based on identity rather than every field, and ORMs often require mutable properties to support persistence. DTOs, request/response models, and domain events should prefer record.

    Rule 3: Prefer switch expressions and pattern matching over long if/else chains

    Many models still generate long chains such as if (x is Foo f && f.Bar > 0). Nested conditional branches are verbose and easy to get wrong. Switch expressions are more compact and can express business rules clearly using relational patterns, logical patterns, and property patterns.

    1. public decimal CalculateFee(Payment p) => p switch
    2. {
    3. { Method: PaymentMethod.Card, Amount: > 1000m } => p.Amount * 0.015m,
    4. { Method: PaymentMethod.Card } => p.Amount * 0.025m,
    5. { Method: PaymentMethod.BankTransfer } => 0m,
    6. _ => throw new ArgumentOutOfRangeException(
    7. nameof(p), p.Method, "Unsupported payment method.")
    8. };
    9. // List pattern and recursive pattern: detect a sequence whose first element is null
    10. public static bool HasLeadingNull(int?[] values) => values is [null, ..];

    The right-hand side of a switch expression must be an expression. As a result, the compiler enforces that the method either returns a value or throws an exception. This structurally prevents a common bug where execution enters a branch but forgets to return. For enums or union-like types, pattern matching with exhaustiveness checking can produce a compiler warning when a new enum value is added, instead of leaving a silent failure path.

    Pattern matching can also go deeper. The following example uses type patterns and property patterns to distinguish domain event types and read the relevant fields without intermediate variables:

    1. public string Describe(OrderEvent evt) => evt switch
    2. {
    3. OrderCreated { CustomerId: var cid, Total: > 0 } => $"new order {cid}",
    4. OrderPaid { Amount: var amount } => $"paid {amount}",
    5. OrderCancelled { Reason: { } reason } => $"cancelled: {reason}",
    6. _ => "unknown event"
    7. };

    2. Async and Concurrency: Remove Hidden Blocking and Race Conditions

    Rule 4: Keep async flows end-to-end

    This is one of the most dangerous categories of AI-generated defects. Common mistakes include reading .Result inside an async method, calling .Wait() or GetAwaiter().GetResult(), or declaring a method as async void. These patterns block the calling thread. In environments with a synchronization context, they can even cause deadlocks. In web applications without a synchronization context, they can exhaust the thread pool and cause throughput to degrade sharply as concurrency increases.

    1. // Bad: blocks the thread and prevents exceptions from being awaited
    2. public OrderDto GetOrder(Guid id) => _db.Orders.Find(id).Result!;
    3. public async void OnOrderPaid(object? sender, EventArgs e) { /* ... */ }
    4. // Good: async all the way, exceptions propagate through the task
    5. public async Task<OrderDto> GetOrderAsync(Guid id, CancellationToken ct)
    6. {
    7. var order = await _db.Orders.FindAsync(id, ct)
    8. ?? throw new OrderNotFoundException(id);
    9. return Map(order);
    10. }

    For these three mistakes, the conventions file should state the following:

    • Async methods must use await from the entry point down to the innermost I/O operation, with no blocking calls in between.
    • Library code generally does not need to know the host context. It may use ConfigureAwait(false) to reduce context-capture overhead. In typical web applications, which have no synchronization context, the performance impact is limited.
    • async void should only be used for event handlers, and even then it must include exception handling.

    Rule 5: Propagate CancellationToken through the entire call chain

    Cancellation is a core part of service governance. When a client disconnects or an upstream request times out, in-flight database queries and HTTP calls should stop as soon as possible so that threads and connections can be used by requests that still matter. AI-generated code often creates a cancellation source only in the innermost I/O method, or ignores cancellation altogether, allowing invalid work to hold resources for a long time.

    1. public async Task<PagedResult<OrderDto>> ListAsync(
    2. string customerId, int page, int size, CancellationToken ct)
    3. {
    4. var query = _db.Orders
    5. .Where(o => o.CustomerId == customerId)
    6. .OrderByDescending(o => o.CreatedAt)
    7. .Skip((page - 1) * size)
    8. .Take(size);
    9. var items = await query.ToListAsync(ct);
    10. var total = await query.CountAsync(ct);
    11. return new PagedResult<OrderDto>(items, total, page, size);
    12. }
    13. // When an independent timeout is needed, use a linked token
    14. // while still honoring the caller's cancellation token
    15. public async Task<Receipt> IssueReceiptAsync(CancellationToken ct)
    16. {
    17. using var timeoutCts = CancellationTokenSource
    18. .CreateLinkedTokenSource(ct);
    19. timeoutCts.CancelAfter(TimeSpan.FromSeconds(3));
    20. return await _billing.IssueAsync(timeoutCts.Token);
    21. }

    The conventions file should require the following:

    • Any API that accepts a CancellationToken must pass that token down to supported downstream calls.
    • Do not create cancellation sources that live indefinitely without either a timeout or a link to an external token.
    • When catching OperationCanceledException, determine whether the cancellation came from the caller or from an internal timeout before deciding how to log or handle it.

    Also distinguish CancellationToken from TimeProvider. CancellationToken represents cancellation, while TimeProvider abstracts time for testable timeout and time-dependent logic. The two should not be used interchangeably.

    Rule 6: Respect service lifetimes and dispose resources properly

    The dependency injection container manages objects as transient, scoped, or singleton. Models easily generate problematic patterns: injecting a scoped service into a singleton, caching mutable state in static fields, or performing I/O in constructors. The result may be captured dependencies that hold disposed objects, data races under concurrency, or slower application startup.

    • Dependency direction: Registration should keep dependencies flowing in one direction. A singleton can depend on a singleton. A scoped service can depend on a scoped or singleton service. The reverse should not be allowed. The built-in ValidateScopes and ValidateOnBuild options can catch these mistakes at startup in development environments.
    • No static mutable state: Prefer framework-provided caching abstractions with sensible capacity limits for local caching, and use a distributed cache for remote data when appropriate.
    • Create-on-demand lightweight objects: If an object is lightweight, safe to create with new, and either inherently thread-safe or internally manages its own thread safety, constructing it directly is fine. It does not need to be injected into DI.

    HTTP clients are a classic exception. Creating them too frequently can exhaust sockets, while registering them as singletons may prevent timely discovery of DNS changes. A more reliable approach is to use a client factory: configure the client once, create clients on demand, and let the container manage handler rotation.

    Resource disposal also needs conventions. Types implementing IDisposable should be wrapped with using or await using. Prefer implementing IAsyncDisposable for asynchronous cleanup instead of synchronous blocking disposal.

    1. // A scoped service owns resource creation and disposal;
    2. // callers use a using declaration to manage its lifetime
    3. public sealed class OrderImportService(
    4. IOrderRepository orders,
    5. IFileStorage storage) : IAsyncDisposable
    6. {
    7. private readonly IAsyncEnumerator<string> _rows =
    8. storage.ReadLinesAsync().GetAsyncEnumerator();
    9. public async ValueTask DisposeAsync() => await _rows.DisposeAsync();
    10. }

    3. Configuration and Exceptions: Turn Implicit Assumptions into Explicit Contracts

    Rule 7: Make configuration strongly typed and validate it at startup

    One of the most common bad patterns in AI-generated code is sprinkling IConfiguration["..."] lookups across business classes. This causes three problems: string keys are scattered everywhere, typos fail silently until runtime, refactoring cannot track references reliably, and unit tests must fake the entire configuration tree.

    The better approach is to bind configuration to strongly typed options classes, validate them with data annotations, and verify them at application startup:

    1. public sealed class PaymentOptions
    2. {
    3. public const string SectionName = "Payments";
    4. [Range(1, 60)]
    5. public int TimeoutSeconds { get; init; } = 10;
    6. [Range(0.0001, 0.1)]
    7. public decimal DefaultFeeRate { get; init; } = 0.02m;
    8. [Required]
    9. public string ApiBaseUrl { get; init; } = "";
    10. }
    11. // Register and validate during application startup
    12. builder.Services.AddOptions<PaymentOptions>()
    13. .Bind(builder.Configuration.GetSection(PaymentOptions.SectionName))
    14. .ValidateDataAnnotations()
    15. .ValidateOnStart();

    ValidateOnStart runs validation when the host starts. If configuration is missing or invalid, the application fails fast instead of waiting until the first payment request. Business code should read configuration through IOptions<T> or IOptionsSnapshot<T>. Use IOptionsSnapshot<T> when configuration needs to refresh dynamically. If configuration contains secrets such as connection strings or API keys, they should be injected by a secret management service or environment variables. They must not be hard-coded into source code.

    Rule 8: Handle exceptions with intent

    Models often wrap large blocks of code with catch (Exception), then return null, an empty collection, or merely log “error occurred.” This allows failures to spread quietly. The caller receives an empty result, assumes the operation completed normally, and the real problem may only be discovered during data reconciliation.

    Exception handling should follow these principles:

    • Handle at the boundary: Catch exceptions only where meaningful recovery, compensation, retry, fallback, or translation into a domain exception is possible. Library code should not log and swallow exceptions.
    • Be specific: Catch the narrowest exception type that matters and use exception filters when appropriate:
    1. try
    2. {
    3. await _cache.SetAsync(key, value, ct);
    4. }
    5. catch (CacheUnavailableException) when (!_cache.IsReadOnly)
    6. {
    7. // Fall back to the database only when the cache is writable
    8. // but temporarily unavailable; let other exceptions propagate
    9. return await _db.LoadAsync(key, ct);
    10. }
    • Preserve the original stack trace: When rethrowing, use throw; instead of throw ex;. Rethrowing with ex replaces the original stack trace.
    • Treat exceptions as contracts: Exception types should express what the caller can do. For example, OrderNotFoundException can map to HTTP 404, while DomainValidationException can map to HTTP 400. Convert exceptions into responses at the outermost web layer.
    • List forbidden patterns: The conventions file should explicitly prohibit empty catch blocks, returning null after catching, broad catch (Exception) blocks that swallow failures, and relying on exception message text for control flow.

    4. Logging and Data Access: Key Defenses for Observability and Performance

    Rule 9: Use structured logging

    Models often generate logging such as $"Order {id} failed". The text is still emitted, but structured fields are lost. In a log platform, you cannot aggregate by OrderId or build field-based alerts. Some models also manually assemble timestamps and log levels, duplicating what the logging framework already does and creating inconsistencies.

    The conventions file should require ILogger<T> and message templates. Named placeholders in message templates become structured fields, and values such as numbers and dates are serialized according to standard formatting rules.

    1. public async Task<OrderDto> GetAsync(Guid id, CancellationToken ct)
    2. {
    3. try
    4. {
    5. return await _queries.FindOrderAsync(id, ct);
    6. }
    7. catch (OrderNotFoundException)
    8. {
    9. _logger.LogWarning("Order {OrderId} not found for request {TraceId}",
    10. id, Activity.Current?.TraceId.ToString() ?? "none");
    11. throw;
    12. }
    13. }

    The conventions file should also require the following:

    • Do not log secrets such as passwords, tokens, ID numbers, or bank card numbers. Mask them if necessary.
    • Assign log levels carefully. High-frequency success paths should use Debug-level logging and sampling to avoid overwhelming disk or log-collection pipelines.
    • Prefer high-performance log methods generated with LoggerMessage for hot paths. They are usually cheaper than template-based logging APIs.

    Rule 10: Follow five data-access rules

    The data access layer is often where AI-generated defects concentrate. Common problems include materializing composable queries into entity lists too early and filtering in memory, querying inside loops and creating N+1 patterns, writing inside loops, forgetting to disable change tracking for read-only queries, and paginating without a stable sort order. These problems are invisible with small datasets but can exhaust database connection pools under load.

    1. // Pagination: stable ordering, cancellation support, and no change tracking
    2. var query = _db.Orders
    3. .AsNoTracking()
    4. .Where(o => o.CustomerId == customerId)
    5. .OrderByDescending(o => o.CreatedAt)
    6. .ThenBy(o => o.Id)
    7. .Skip((page - 1) * size)
    8. .Take(size);
    9. // Related data: project to DTOs, select only required columns,
    10. // and avoid loading the entire object graph into memory
    11. var items = await query
    12. .Select(o => new OrderSummaryDto(o.Id, o.Total, o.Status, o.Lines.Count))
    13. .ToListAsync(ct);
    14. // Fix N+1: batch-load collections instead of querying one row at a time
    15. var orderIds = items.Select(i => i.Id).ToArray();
    16. var linesByOrder = await _db.Lines
    17. .AsNoTracking()
    18. .Where(l => orderIds.Contains(l.OrderId))
    19. .GroupBy(l => l.OrderId)
    20. .ToDictionaryAsync(g => g.Key, g => g.ToList(), ct);

    The five rules are:

    1. Materialize composable IQueryable queries at the right time: Keep deferred execution when the query must be composed further. Materialize immediately when you need to cache, count, or return data to the caller.
    2. Filter before projecting: Push filtering to the database and select only the required fields.
    3. Prefer batch operations over loops: Use Contains, temporary tables, or bulk-write APIs instead of per-row access. If a loop needs cross-service data, prefer a batch endpoint.
    4. Make tracking explicit: Disable change tracking for read-only queries. When updates are required, load by aggregate root to avoid partial updates that cause concurrency overwrites.
    5. Make sorting and filtering index-friendly: Create covering indexes for filter and sort fields. For deep pagination, prefer keyset pagination based on cursors.

    All dynamic queries should also be parameterized. Never build SQL with string concatenation; use parameterization to prevent injection.

    5. Modern Language Features and Collection Performance

    Rule 11: Prefer modern language features that express intent

    Recent versions of C# include many features that improve clarity and safety. Models tend to follow the “average style” of their training data, so they may not use these features proactively. The conventions file can list them explicitly:

    1. // Primary constructor and automatic property initializer:
    2. // dependencies become fields without manual assignment
    3. public sealed class OrderFacade(
    4. IOrderRepository repository,
    5. TimeProvider timeProvider)
    6. {
    7. // required: forces the caller to assign this member in an object initializer
    8. public sealed record CreateRequest
    9. {
    10. public required Guid CustomerId { get; init; }
    11. public required string Currency { get; init; }
    12. }
    13. // Collection expression and raw string literal
    14. private static readonly string[] ZeroFeeMethods = ["BankTransfer", "InternalCredit"];
    15. public OrderCreated Create(CreateRequest request) => new(
    16. Guid.NewGuid(),
    17. request.CustomerId,
    18. timeProvider.GetUtcNow());
    19. }

    required and init make “which fields must be set” and “objects are immutable after construction” compiler-verifiable contracts. Raw string literals are useful for SQL, JSON templates, and other content containing quotes. Collection expressions make array and list initialization more concise.

    Several modern APIs should also be considered when retrieving or rewriting code:

    • FrozenDictionary: Suitable for read-only lookup tables that do not change after process startup, with lower lookup overhead.
    • TimeProvider: Makes time-dependent logic testable with fake clocks.
    • System.Threading.Channels: Useful for producer/consumer pipelines with backpressure.
    • Parallel.ForEachAsync: Suitable for parallel execution of independent async tasks.

    Rule 12: Write collections and LINQ with performance in mind

    The biggest LINQ pitfall is deferred execution combined with multiple enumeration. If an IEnumerable<T> variable is consumed by foreach, Count(), and ToList(), the query may execute multiple times. AI-generated code often contains this pattern:

    1. // Bad: Count() triggers a full enumeration, then foreach enumerates again
    2. if (orders.Count() > 0)
    3. {
    4. foreach (var o in orders) { /* ... */ }
    5. }
    6. // Good: use Any() for existence checks, or materialize once if reused
    7. var snapshot = orders as IReadOnlyList<Order> ?? orders.ToList();
    8. if (snapshot.Count > 0)
    9. {
    10. foreach (var o in snapshot) { /* ... */ }
    11. }

    Other important points for the conventions file include:

    • Existence checks: Use Any() instead of Count() > 0.
    • Membership checks: Use HashSet<T> instead of calling Contains on a list inside a loop, which is a linear scan.
    • Reduce allocations: Use StringBuilder for string concatenation. When parsing fixed-length protocols or avoiding allocations, consider ReadOnlySpan<char> and its parsing helpers.
    • Do not materialize large collections and filter in memory: Filter at the query source whenever possible.
    • Require benchmarks for performance work: Any optimization involving allocations or latency should be supported by benchmarks.

    6. Security Guardrails and Engineering Enforcement

    Rule 13: Default to secure behavior and automate guardrails

    AI-generated endpoints are often functionally complete but lack authorization declarations. Models also tend to generate code that “works first,” which can mean placing secrets in configuration files, logging full exception stacks in catch blocks, or deserializing untrusted input with unsafe settings. The conventions file should define a minimum secure-by-default baseline:

    • Every new web endpoint must declare an authorization policy explicitly and deny anonymous access by default.
    • Input from clients or external systems must be validated for length, format, and range.
    • Never put credentials in source code, configuration files, or logs. Inject them through secret management services or environment variables.
    • Error responses should return only necessary error codes and messages. Stack traces belong in server-side logs only.
    • Pin new dependencies to specific versions and run dependency vulnerability scanning in the pipeline.
    • Configure runtime identity and network policies with least privilege. This principle applies whether the application runs on a public cloud container platform or in a self-hosted environment.

    A conventions file also needs hard enforcement. Written guidance can steer models, but it cannot prevent a generated change from quietly bypassing the rules. The more reliable approach is to turn rules into automated guardrails in .editorconfig and the CI pipeline:

    1. # Example CI pipeline steps
    2. - run: dotnet format --verify-no-changes # formatting and code style consistency
    3. - run: dotnet build --configuration Release -warnaserror # treat warnings as errors
    4. - run: dotnet test --collect:"XPlat Code Coverage" # unit tests and coverage

    --verify-no-changes checks whether formatting and style rules were followed and exits with a non-zero status when changes are required. -warnaserror ensures issues such as nullability warnings are caught in CI instead of depending on one developer’s IDE settings.

    For async and blocking defects, add a dedicated unit test: run the method under test in a single-threaded synchronization context. If the method contains a blocking async call, the test should hang and eventually fail by timeout.

    1. [Fact]
    2. public async Task GetOrderAsync_ShouldNotBlockSynchronizationContext()
    3. {
    4. var service = CreateServiceWithFakeRepository();
    5. using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
    6. var task = Task.Run(
    7. () => service.GetOrderAsync(Guid.NewGuid(), CancellationToken.None),
    8. cts.Token);
    9. await task.WaitAsync(cts.Token);
    10. Assert.True(task.IsCompletedSuccessfully);
    11. }

    How to Organize the Conventions File

    A conventions file is not just a pile of rules. The longer it becomes, the easier it is for critical constraints to get lost. A practical structure is:

    1. # Project Coding Conventions for Humans and AI Coding Assistants
    2. ## 1. Principles
    3. - Rules must be verifiable, executable, and aligned with CI guardrails.
    4. - If a rule conflicts with a CI result, the CI result wins.
    5. ## 2. Rules 1-13
    6. Each rule should include:
    7. - Purpose
    8. - Good example
    9. - Bad example
    10. - Enforcement method: compiler, analyzer, test, or review
    11. ## 3. Reference Implementation
    12. Paths to example projects and unit tests that demonstrate core patterns.
    13. ## 4. Forbidden List
    14. Examples: .Result, .Wait(), async void, empty catch blocks,
    15. IConfiguration["..."] sprinkled across business code, and string-concatenated queries.

    The conventions file also needs maintenance. Review it regularly as the project evolves. Add new rules promptly when CI introduces a new analyzer rule or code review reveals a recurring issue. Keep the reason for each change in version control history.

    When both team members and AI coding assistants follow the same executable conventions, “the code runs” gradually becomes “the code can be shipped with confidence.”