agentleFS
linuxchatalinuxchata/ai-playbook/.claude/skills/csharp-coding-standards/SKILL.md

csharp-coding-standards

Skill6 starsChanged 6 months agoMITView on GitHub

Defines the C# coding standards, patterns, and conventions to be applied consistently across all C# projects. Rules cover naming, structure, async patterns, null handling, dependency injection, logging, result patterns, and formatting. Apply these rules uniformly in all production code.

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

33 rules. About 0 are common, 9 are distinctive, and 24 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 24

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Always use file-scoped namespaces:

    line 20
  2. Very specific:

    Place using directives outside the namespace.

    line 37
  3. Distinctive:

    Use implicit usings where available.

    line 39
  4. Very specific:

    Prefer internal for implementation types.

    line 102
  5. Very specific:

    Avoid the null-forgiving operator (!) without an inline comment explaining why it is safe.

    line 126
  6. Distinctive:

    Prefer early-return guards over deeply nested null checks.

    line 127
  7. Very specific:

    Use modern ArgumentNullException helpers for public/internal method boundaries:

    line 139
  8. Very specific:

    Prefer ?. and ?? / ??= over verbose null checks in expressions:

    line 149
  9. Very specific:

    Always inject dependencies via the constructor.

    line 164
  10. Very specific:

    Every public and internal async method should accept a CancellationToken as its last parameter:

    line 216
  11. Very specific:

    Do not use .ConfigureAwait(false) in application-layer and library code targeting ASP.NET Core.

    line 226
  12. Very specific:

    Never use async void except for event handlers.

    line 230
  13. Very specific:

    Use SemaphoreSlim for async-compatible critical sections.

    line 234
  14. Very specific:

    When a method only delegates to a single async call with no surrounding logic, return the Task directly to avoid an unnecessary state …

    line 252
  15. Distinctive:

    Do not throw exceptions for expected, recoverable validation failures.

    line 268
  16. Distinctive:

    Always use named placeholder syntax – never string interpolation in log messages:

    line 315
  17. Very specific:

    Always inject ILogger<T> where T is the exact enclosing class.

    line 336
  18. Very specific:

    Use C# 12+ collection expressions ([]) for empty collections and simple inline initialization:

    line 344
  19. Distinctive:

    Prefer method syntax over query syntax.

    line 355
  20. Very specific:

    Avoid intermediate .ToList() calls mid-chain.

    line 357
  21. Very specific:

    Prefer .Length == 0 or .Count == 0 over !collection.Any() in performance-sensitive paths.

    line 369
  22. Very specific:

    Use switch expressions for exhaustive value dispatch instead of chains of if/else if:

    line 375
  23. Very specific:

    Prefer is null / is not null over == null / != null for reference type null checks:

    line 388
  24. Very specific:

    Use the required keyword for mandatory properties to enforce initialization at the call site:

    line 401
  25. Very specific:

    Never use magic numbers or strings inline.

    line 449
  26. Very specific:

    Always use braces, even for single-line if / else / for / foreach bodies:

    line 474
  27. Distinctive:

    Always use object initializer syntax for domain objects and DTOs.

    line 513
  28. Very specific:

    Place extension methods in a dedicated Extensions/ folder.

    line 529
  29. Very specific:

    All extension classes are public static.

    line 531
  30. Distinctive:

    Never log raw sensitive bytes, private key material, passwords, tokens, or PII.

    line 547
  31. Very specific:

    Use constant-time or span-based byte array comparison helpers to prevent timing attacks – never use == or SequenceEqual for …

    line 548
  32. Very specific:

    Always sanitize user input (.Trim()) before storing, comparing, or forwarding.

    line 549
  33. Distinctive:

    Before submitting any C# file, verify:

    line 556

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

C# Coding Standards

Description

Defines the C# coding standards, patterns, and conventions to be applied consistently across all C# projects. Rules cover naming, structure, async patterns, null handling, dependency injection, logging, result patterns, and formatting. Apply these rules uniformly in all production code.


1. File & Namespace Structure

1.1 File-Scoped Namespaces

Always use file-scoped namespaces:

// ✅ Correct
namespace MyApp.Core.Validators;

internal class OrderValidator { }

// ❌ Wrong
namespace MyApp.Core.Validators
{
    internal class OrderValidator { }
}

1.2 Using Directives

  • Place using directives outside the namespace.
  • Group: System namespaces first, then others ordered alphabetically – separated by a blank line.
  • Use implicit usings where available; add explicit usings only when needed for disambiguation.
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using MyApp.Core.Abstractions;
using MyApp.Domain;

1.3 One Type Per File

Each file contains exactly one top-level type. The file name must match the type name exactly.


2. Naming Conventions

2.1 Types

KindConventionExample
ClassesPascalCaseOrderValidator, UserService
InterfacesI prefix + PascalCaseIOrderRepository, IUserService
EnumsPascalCaseOrderStatus, PaymentMethod
StructsPascalCaseMoney, DateRange
RecordsPascalCaseAppConfiguration, UserSettings

2.2 Members

KindConventionExample
Public propertiesPascalCaseCreatedAt, TotalAmount
Public methodsPascalCaseCreateOrder, ValidatePayment
Private fields_camelCase (underscore prefix)_repository, _logger
ConstantsPascalCaseDefaultTimeout, MaxRetryCount
Local variablescamelCaseorderId, serializedData
Method parameterscamelCasecancellationToken, userId
Lambda parametersShort camelCasex =>, e =>, o =>

2.3 Async Methods

All async methods MUST end with the Async suffix without exception.

// ✅ Correct
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken);

// ❌ Wrong – missing Async suffix
public async Task<Order?> GetById(Guid id, CancellationToken cancellationToken);

This applies to:

  • Interface declarations
  • Public and internal implementation methods
  • Private helper methods

3. Access Modifiers & Sealed Classes

3.1 Least Privilege

  • Prefer internal for implementation types; use public only for types that are part of the public API surface.
  • Implementation classes registered via DI should be internal sealed.
  • Types that implement a public interface and form the public API are public sealed.
// ✅ Correct: public sealed for types exposed via public interface
public sealed class OrderService : IOrderService { }

// ✅ Correct: internal sealed for infrastructure implementations
internal sealed class SqlOrderRepository : IOrderRepository { }

3.2 sealed Usage

Mark classes sealed by default unless inheritance is explicitly required. This prevents unintended derivation and enables JIT optimizations.


4. Null Handling & Defensive Programming

4.1 Nullable Reference Types

  • Enable nullable context for all projects (<Nullable>enable</Nullable>).
  • Annotate all nullable references explicitly with ?.
  • Avoid the null-forgiving operator (!) without an inline comment explaining why it is safe.
  • Prefer early-return guards over deeply nested null checks.
// ✅ Early return guard
if (id is null || id.Length == 0)
{
    return null;
}

4.2 Argument Validation

Use modern ArgumentNullException helpers for public/internal method boundaries:

// ✅ Correct
ArgumentNullException.ThrowIfNull(order);
ArgumentNullException.ThrowIfNullOrEmpty(order.UserName);

4.3 Null Propagation & Coalescing

Prefer ?. and ?? / ??= over verbose null checks in expressions:

// ✅ Correct
return items?.Select(x => x.ToDto()).ToArray();

var name = input?.Trim() ?? string.Empty;

5. Constructor Injection & Dependency Injection

5.1 Constructor Injection

Always inject dependencies via the constructor. Store them as private readonly fields prefixed with _.

private readonly IOrderRepository _orderRepository;
private readonly ILogger<OrderService> _logger;

public OrderService(IOrderRepository orderRepository, ILogger<OrderService> logger)
{
    _orderRepository = orderRepository;
    _logger = logger;
}

5.2 Options Pattern

Access configuration via IOptions<T>. Extract .Value in the constructor and store as a field – do not access .Value repeatedly at call sites.

private readonly AppConfiguration _configuration;

public OrderService(IOptions<AppConfiguration> options)
{
    _configuration = options.Value;
}

5.3 DI Registration

Centralize all service registrations in a dedicated DependencyInjection.cs static class with IServiceCollection extension methods.

public static class DependencyInjection
{
    public static void AddMyFeature(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddTransient<IOrderService, OrderService>();
        services.AddKeyedTransient<IPaymentStrategy, CreditCardStrategy>("creditcard");
    }
}

Lifetime guidance:

  • AddTransient – stateless, short-lived services.
  • AddSingleton – thread-safe, shared-state services (e.g., TimeProvider, static caches).
  • AddKeyedTransient / AddKeyedSingleton – strategy-pattern registrations keyed by a discriminator.

6. Async / Await

6.1 Always Propagate CancellationToken

Every public and internal async method should accept a CancellationToken as its last parameter:

// ✅ Correct
Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
Task SaveAsync(Order order, CancellationToken cancellationToken);

6.2 No ConfigureAwait(false) in Application Code

Do not use .ConfigureAwait(false) in application-layer and library code targeting ASP.NET Core. Reserve it only for low-level infrastructure libraries where synchronization context must be avoided explicitly.

6.3 Never Use async void

Never use async void except for event handlers. Always return Task or Task<T>.

6.4 Async Locks via SemaphoreSlim

Use SemaphoreSlim for async-compatible critical sections. Always release in a finally block:

private static readonly SemaphoreSlim Lock = new(1, 1);

await Lock.WaitAsync(cancellationToken);
try
{
    // Critical section
}
finally
{
    Lock.Release();
}

6.5 Return Task Directly for Pass-Through Methods

When a method only delegates to a single async call with no surrounding logic, return the Task directly to avoid an unnecessary state machine:

// ✅ Correct – no state machine overhead
private Task<string?> GetCachedValueAsync(string key, CancellationToken cancellationToken)
{
    return _cache.GetStringAsync(key, cancellationToken);
}

7. Result Pattern

7.1 Internal Results

Do not throw exceptions for expected, recoverable validation failures. Use typed result objects that carry success/failure state and an optional message:

// Validator result (IsValid + Message)
public static ValidationResult Valid() => new(true);
public static ValidationResult Invalid(string message) => new(false, message);

// Handler/operation result (Value + HasError + Message)
public Result<T> Execute(...)
{
    if (inputIsInvalid)
        return new Result<T>("Validation failed: input cannot be null");

    return new Result<T>(computedValue);
}

7.2 Public Results

Public-facing operation results use static factory methods for readability:

// ✅ Correct
return OperationResult.Success();
return OperationResult.Failure("The record was not found");

7.3 Fail Fast, Return Early

Validate all inputs at the top of the method. Return early on error. Do not let invalid state propagate deep into a method body.

var validation = _validator.Validate(request);
if (!validation.IsValid)
{
    return OperationResult.Failure(validation.Message!);
}

// proceed with valid data only

8. Logging

8.1 Structured Logging

Always use named placeholder syntax – never string interpolation in log messages:

// ✅ Correct
_logger.LogWarning("Order '{OrderId}' was not found for user '{UserId}'", orderId, userId);

// ❌ Wrong
_logger.LogWarning($"Order '{orderId}' was not found for user '{userId}'");

8.2 Log Level Selection

SituationLevel
Normal successful operationLogDebug
Handled failure or unexpected but recoverable conditionLogWarning
Unrecoverable exception or system errorLogError
Sensitive / PII dataDo not log – redact

8.3 Logger Injection

Always inject ILogger<T> where T is the exact enclosing class. Do not share logger instances across types.


9. Collections & LINQ

9.1 Collection Expressions

Use C# 12+ collection expressions ([]) for empty collections and simple inline initialization:

// ✅ Correct
string[] tags = [];
Items = existingItems ?? [],
return [];

9.2 LINQ Style

  • Prefer method syntax over query syntax.
  • Chain transformations fluently (.Where(), .Select(), .OrderBy()) and materialize at the end with .ToList() or .ToArray().
  • Avoid intermediate .ToList() calls mid-chain; defer materialization.
// ✅ Correct
return orders
    .Where(o => o.IsActive)
    .Select(o => o.ToSummaryDto())
    .ToArray();

9.3 Empty Collection Checks

Prefer .Length == 0 or .Count == 0 over !collection.Any() in performance-sensitive paths.


10. Pattern Matching & Switch Expressions

Use switch expressions for exhaustive value dispatch instead of chains of if/else if:

// ✅ Correct
var label = status switch
{
    OrderStatus.Pending   => "Awaiting confirmation",
    OrderStatus.Confirmed => "Confirmed",
    OrderStatus.Shipped   => "On the way",
    _                     => "Unknown",
};

Prefer is null / is not null over == null / != null for reference type null checks:

if (order is null) { ... }
if (result is not null) { ... }

11. Domain Model Design

11.1 Required Properties

Use the required keyword for mandatory properties to enforce initialization at the call site:

public sealed class Order
{
    public required Guid Id { get; set; }
    public required string CustomerName { get; set; }
    public required decimal TotalAmount { get; set; }
}

11.2 Sealed Domain Models

Seal domain and result classes that are not designed for inheritance:

public sealed class Order { }
public sealed class OperationResult { }

12. XML Documentation

12.1 Public API

All public interfaces, classes, and their members must carry full XML doc comments:

/// <summary>
/// Retrieves an order by its unique identifier.
/// </summary>
/// <param name="id">The unique identifier of the order.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The order if found; otherwise, <c>null</c>.</returns>
Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken);

12.2 Internal Types

XML doc comments are optional on internal types but strongly encouraged for any non-trivial logic.


13. Constants & Magic Values

13.1 Named Constants

Never use magic numbers or strings inline. Declare them as private const or private static readonly:

private const int MaxRetryCount = 3;
private const string DefaultCacheKeyPrefix = "order";
private const int DefaultTimeoutSeconds = 60;

13.2 Constants Organization

Group related constants into dedicated static classes inside a Constants/ folder. One class per logical domain (e.g., CacheKeys, HeaderNames, ErrorMessages).


14. Formatting

14.1 Indentation & Whitespace

  • Indent size: 4 spaces – no tabs.
  • Line endings: CRLF.
  • One blank line between method bodies.
  • One blank line between property groups.

14.2 Braces

Always use braces, even for single-line if / else / for / foreach bodies:

// ✅ Correct
if (order is null)
{
    return OperationResult.Failure("Order not found");
}

// ❌ Wrong
if (order is null)
    return OperationResult.Failure("Order not found");

14.3 Expression-Bodied Members

Member TypePreferred Style
PropertiesExpression-bodied (=>) allowed
Short indexersExpression-bodied allowed
MethodsBlock body preferred
ConstructorsBlock body required
LambdasExpression-bodied preferred
Local functionsBlock body preferred

14.4 Long Parameter Lists

Break constructor or method parameters onto separate lines when there are 3 or more:

public OrderService(
    IOrderRepository orderRepository,
    IPaymentService paymentService,
    IOptions<AppConfiguration> options,
    ILogger<OrderService> logger)

14.5 Object & Collection Initializers

Always use object initializer syntax for domain objects and DTOs. Include a trailing comma after the last member:

var order = new Order
{
    Id = Guid.NewGuid(),
    CustomerName = request.CustomerName.Trim(),
    TotalAmount = request.Amount,
    Tags = [],
};

15. Extension Methods

  • Place extension methods in a dedicated Extensions/ folder.
  • One static class per extended type or logical concept: StringExtensions, EnumExtensions, DateTimeExtensions.
  • All extension classes are public static.
  • Generic constraints should be as tight as the use case requires.
public static class EnumExtensions
{
    public static string GetSerializedValue<T>(this T value) where T : Enum { ... }
    public static T ToEnum<T>(this string value) where T : struct, Enum { ... }
    public static T? ToNullableEnum<T>(this string? value) where T : struct, Enum { ... }
}

16. Security-Sensitive Code

  • Never log raw sensitive bytes, private key material, passwords, tokens, or PII.
  • Use constant-time or span-based byte array comparison helpers to prevent timing attacks – never use == or SequenceEqual for security-critical comparisons.
  • Always sanitize user input (.Trim()) before storing, comparing, or forwarding.
  • Validate all untrusted input at the boundary layer – do not rely on inner layers to catch invalid data.

Quick Reference Checklist

Before submitting any C# file, verify:

  • File-scoped namespace used
  • All async methods end with the Async suffix
  • CancellationToken is the last parameter of every async method
  • Nullable reference types annotated correctly – no unexplained !
  • Braces on every if / else / for / foreach
  • No magic numbers or strings – all constants named
  • Structured logging – no string interpolation in log calls
  • Public API members have XML documentation
  • Result pattern used instead of exceptions for expected failures

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.