csharp-coding-standards-4f52f4 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited csharp-coding-standards-4f52f4 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
Always use file-scoped namespaces:
// ✅ Correct
namespace MyApp.Core.Validators;
internal class OrderValidator { }
// ❌ Wrong
namespace MyApp.Core.Validators
{
internal class OrderValidator { }
}using directives outside the namespace.using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using MyApp.Core.Abstractions;
using MyApp.Domain;Each file contains exactly one top-level type. The file name must match the type name exactly.
| Kind | Convention | Example |
|---|---|---|
| Classes | PascalCase | OrderValidator, UserService |
| Interfaces | I prefix + PascalCase | IOrderRepository, IUserService |
| Enums | PascalCase | OrderStatus, PaymentMethod |
| Structs | PascalCase | Money, DateRange |
| Records | PascalCase | AppConfiguration, UserSettings |
| Kind | Convention | Example |
|---|---|---|
| Public properties | PascalCase | CreatedAt, TotalAmount |
| Public methods | PascalCase | CreateOrder, ValidatePayment |
| Private fields | _camelCase (underscore prefix) | _repository, _logger |
| Constants | PascalCase | DefaultTimeout, MaxRetryCount |
| Local variables | camelCase | orderId, serializedData |
| Method parameters | camelCase | cancellationToken, userId |
| Lambda parameters | Short camelCase | x =>, e =>, o => |
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:
internal for implementation types; use public only for types that are part of the public API surface.internal sealed.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 { }sealed UsageMark classes sealed by default unless inheritance is explicitly required. This prevents unintended derivation and enables JIT optimizations.
<Nullable>enable</Nullable>).?.!) without an inline comment explaining why it is safe.// ✅ Early return guard
if (id is null || id.Length == 0)
{
return null;
}Use modern ArgumentNullException helpers for public/internal method boundaries:
// ✅ Correct
ArgumentNullException.ThrowIfNull(order);
ArgumentNullException.ThrowIfNullOrEmpty(order.UserName);Prefer ?. and ?? / ??= over verbose null checks in expressions:
// ✅ Correct
return items?.Select(x => x.ToDto()).ToArray();
var name = input?.Trim() ?? string.Empty;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;
}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;
}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.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);ConfigureAwait(false) in Application CodeDo 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.
async voidNever use async void except for event handlers. Always return Task or Task<T>.
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();
}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);
}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);
}Public-facing operation results use static factory methods for readability:
// ✅ Correct
return OperationResult.Success();
return OperationResult.Failure("The record was not found");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 onlyAlways 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}'");| Situation | Level |
|---|---|
| Normal successful operation | LogDebug |
| Handled failure or unexpected but recoverable condition | LogWarning |
| Unrecoverable exception or system error | LogError |
| Sensitive / PII data | Do not log – redact |
Always inject ILogger<T> where T is the exact enclosing class. Do not share logger instances across types.
Use C# 12+ collection expressions ([]) for empty collections and simple inline initialization:
// ✅ Correct
string[] tags = [];
Items = existingItems ?? [],
return [];.Where(), .Select(), .OrderBy()) and materialize at the end with .ToList() or .ToArray()..ToList() calls mid-chain; defer materialization.// ✅ Correct
return orders
.Where(o => o.IsActive)
.Select(o => o.ToSummaryDto())
.ToArray();Prefer .Length == 0 or .Count == 0 over !collection.Any() in performance-sensitive paths.
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) { ... }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; }
}Seal domain and result classes that are not designed for inheritance:
public sealed class Order { }
public sealed class OperationResult { }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);XML doc comments are optional on internal types but strongly encouraged for any non-trivial logic.
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;Group related constants into dedicated static classes inside a Constants/ folder. One class per logical domain (e.g., CacheKeys, HeaderNames, ErrorMessages).
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");| Member Type | Preferred Style |
|---|---|
| Properties | Expression-bodied (=>) allowed |
| Short indexers | Expression-bodied allowed |
| Methods | Block body preferred |
| Constructors | Block body required |
| Lambdas | Expression-bodied preferred |
| Local functions | Block body preferred |
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)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 = [],
};Extensions/ folder.StringExtensions, EnumExtensions, DateTimeExtensions.public static.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 { ... }
}== or SequenceEqual for security-critical comparisons..Trim()) before storing, comparing, or forwarding.Before submitting any C# file, verify:
Async suffixCancellationToken is the last parameter of every async method!if / else / for / foreach~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.