Modern Csharp Coding Standards logo

Modern Csharp Coding Standards

Community
wshaddix
modern-csharp-coding-standards

Write modern, high-performance C# code using records, pattern matching, value objects, async/await, Span<T>/Memory<T>, and best-practice API design patterns. Emphasizes functional-style programming with C# 12+ features. Use when writing new C# code or refactoring existing code, designing public APIs for libraries or services, optimizing performance-critical code paths, or building async/await-heavy applications.

Overview

Publisherwshaddix
Repositorydotnet-skills
Skill namemodern-csharp-coding-standards
Stars
79
Forks
13
Bundled files
5
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 5 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by wshaddix on GitHub. Read the source before you install it.

Installation

Install the Modern Csharp Coding Standards AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/wshaddix/dotnet-skills.git /tmp/dotnet-skills
mkdir -p .claude/skills
cp -r /tmp/dotnet-skills/skills/csharp-coding-standards .claude/skills/modern-csharp-coding-standards
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Modern Csharp Coding Standards in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Modern Csharp Coding Standards on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Modern Csharp Coding Standards is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Modern C# Coding Standards

When to Use This Skill

Use this skill when:

  • Writing new C# code or refactoring existing code
  • Designing public APIs for libraries or services
  • Optimizing performance-critical code paths
  • Implementing domain models with strong typing
  • Building async/await-heavy applications
  • Working with binary data, buffers, or high-throughput scenarios

Core Principles

  1. Immutability by Default - Use record types and init-only properties
  2. Type Safety - Leverage nullable reference types and value objects
  3. Modern Pattern Matching - Use switch expressions and patterns extensively
  4. Async Everywhere - Prefer async APIs with proper cancellation support
  5. Zero-Allocation Patterns - Use Span<T> and Memory<T> for performance-critical code
  6. API Design - Accept abstractions, return appropriately specific types
  7. Composition Over Inheritance - Avoid abstract base classes, prefer composition
  8. Value Objects as Structs - Use readonly record struct for value objects

Naming Conventions

General Rules

ElementConventionExample
NamespacesPascalCase, dot-separatedMyCompany.MyProduct.Core
Classes, Records, StructsPascalCaseOrderService, OrderSummary
InterfacesI + PascalCaseIOrderRepository
MethodsPascalCaseGetOrderAsync
PropertiesPascalCaseOrderDate
EventsPascalCaseOrderCompleted
Public constantsPascalCaseMaxRetryCount
Private fields_camelCase_orderRepository
Parameters, localscamelCaseorderId, totalAmount
Type parametersT or T + PascalCaseT, TKey, TValue
Enum membersPascalCaseOrderStatus.Pending

Async Method Naming

Suffix async methods with Async:

csharp
public Task<Order> GetOrderAsync(int id);
public ValueTask SaveChangesAsync(CancellationToken ct);

Exception: Event handlers and interface implementations where the framework does not use the `Async` suffix (e.g., ASP.NET Core middleware `InvokeAsync` is already named by the framework).

Boolean Naming

Prefix booleans with is, has, can, should, or similar:

csharp
public bool IsActive { get; set; }
public bool HasOrders { get; }
public bool CanDelete(Order order);

Collection Naming

Use plural nouns for collections:

csharp
public IReadOnlyList<Order> Orders { get; }
public Dictionary<string, int> CountsByName { get; }

File Organization

One Type Per File

Each top-level type (class, record, struct, interface, enum) should be in its own file, named exactly as the type. Nested types stay in the containing type's file.

OrderService.cs        -> public class OrderService
IOrderRepository.cs    -> public interface IOrderRepository
OrderStatus.cs         -> public enum OrderStatus
OrderSummary.cs        -> public record OrderSummary

File-Scoped Namespaces

Always use file-scoped namespaces (C# 10+):

csharp
namespace MyApp.Services;

public class OrderService { }

Using Directives

Place using directives at the top of the file, outside the namespace. With <ImplicitUsings>enable</ImplicitUsings> (default in modern .NET), common namespaces are already imported.

Order of using directives:

  1. System.* namespaces
  2. Third-party namespaces
  3. Project namespaces

Code Style

Braces

Always use braces for control flow, even for single-line bodies:

csharp
if (order.IsValid)
{
    Process(order);
}

Expression-Bodied Members

Use expression bodies for single-expression members:

csharp
public string FullName => $"{FirstName} {LastName}";
public override string ToString() => $"Order #{Id}";

var Usage

Use var when the type is obvious from the right-hand side:

csharp
var orders = new List<Order>();
var customer = GetCustomerById(id);

IOrderRepository repo = serviceProvider.GetRequiredService<IOrderRepository>();
decimal total = CalculateTotal(items);

Null Handling

Prefer pattern matching over null checks:

csharp
if (order is not null) { }
if (order is { Status: OrderStatus.Active }) { }

var name = customer?.Name ?? "Unknown";
var orders = customer?.Orders ?? [];
items ??= [];

String Handling

Prefer string interpolation over concatenation or string.Format:

csharp
var message = $"Order {orderId} totals {total:C2}";

var json = $$"""
    {
        "id": {{orderId}},
        "name": "{{name}}"
    }
    """;

Access Modifiers

Always specify access modifiers explicitly. Do not rely on defaults:

csharp
public class OrderService
{
    private readonly IOrderRepository _repo;
    internal void ProcessBatch() { }
}

Modifier Order

access (public/private/protected/internal) -> static -> extern -> new ->
virtual/abstract/override/sealed -> readonly -> volatile -> async -> partial
csharp
public static readonly int MaxSize = 100;
protected virtual async Task<Order> LoadAsync() => await repo.GetDefaultAsync();
public sealed override string ToString() => Name;

Type Design

Seal Classes by Default

Seal classes that are not designed for inheritance. This improves performance (devirtualization) and communicates intent:

csharp
public sealed class OrderService(IOrderRepository repo)
{
}

Only leave classes unsealed when you explicitly design them as base classes.

Prefer Composition Over Inheritance

csharp
public sealed class OrderProcessor(IValidator validator, INotifier notifier)
{
    public async Task ProcessAsync(Order order)
    {
        await validator.ValidateAsync(order);
        await notifier.NotifyAsync(order);
    }
}

Interface Segregation

Keep interfaces focused. Prefer multiple small interfaces over one large one:

csharp
public interface IOrderReader
{
    Task<Order?> GetByIdAsync(int id, CancellationToken ct = default);
    Task<IReadOnlyList<Order>> GetAllAsync(CancellationToken ct = default);
}

public interface IOrderWriter
{
    Task<Order> CreateAsync(Order order, CancellationToken ct = default);
    Task UpdateAsync(Order order, CancellationToken ct = default);
}

Language Patterns

See Language Patterns for detailed guidance on:

  • Records for Immutable Data (C# 9+)
  • Value Objects as readonly record struct
  • Pattern Matching (C# 8-12)
  • Nullable Reference Types (C# 8+)
  • Composition Over Inheritance

Performance Patterns

See Performance Patterns for detailed guidance on:

  • Async/Await Best Practices
  • Span and Memory for Zero-Allocation Code

API Design Principles

See API Design Principles for detailed guidance on:

  • Accept Abstractions, Return Appropriately Specific
  • Method Signatures Best Practices

Error Handling

See Error Handling for detailed guidance on:

  • Result Type Pattern (Railway-Oriented Programming)

Testing Patterns

csharp
public record OrderBuilder
{
    public OrderId Id { get; init; } = OrderId.New();
    public CustomerId CustomerId { get; init; } = CustomerId.New();
    public Money Total { get; init; } = new Money(100m, "USD");
    public IReadOnlyList<OrderItem> Items { get; init; } = Array.Empty<OrderItem>();

    public Order Build() => new(Id, CustomerId, Total, Items);
}

[Fact]
public void CalculateDiscount_LargeOrder_AppliesCorrectDiscount()
{
    var baseOrder = new OrderBuilder().Build();
    var largeOrder = baseOrder with { Total = new Money(1500m, "USD") };

    var discount = _service.CalculateDiscount(largeOrder);

    discount.Should().Be(new Money(225m, "USD"));
}

[Theory]
[InlineData("ORD-12345", true)]
[InlineData("INVALID", false)]
public void TryParseOrderId_VariousInputs_ReturnsExpectedResult(
    string input, bool expected)
{
    var result = OrderIdParser.TryParse(input.AsSpan(), out var orderId);
    result.Should().Be(expected);
}

[Fact]
public void Money_Add_SameCurrency_ReturnsSum()
{
    var money1 = new Money(100m, "USD");
    var money2 = new Money(50m, "USD");

    var result = money1.Add(money2);

    result.Should().Be(new Money(150m, "USD"));
}

[Fact]
public void Money_Add_DifferentCurrency_ThrowsException()
{
    var usd = new Money(100m, "USD");
    var eur = new Money(50m, "EUR");

    var act = () => usd.Add(eur);
    act.Should().Throw<InvalidOperationException>()
        .WithMessage("*different currencies*");
}

CancellationToken Conventions

Accept CancellationToken as the last parameter in async methods. Use default as the default value for optional tokens:

csharp
public async Task<Order> GetOrderAsync(int id, CancellationToken ct = default)
{
    return await _repo.GetByIdAsync(id, ct);
}

Always forward the token to downstream async calls. Never ignore a received CancellationToken.


XML Documentation

Add XML docs to public API surfaces. Keep them concise:

csharp
/// <summary>
/// Retrieves an order by its unique identifier.
/// </summary>
/// <param name="id">The order identifier.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The order, or <see langword="null"/> if not found.</returns>
public Task<Order?> GetByIdAsync(int id, CancellationToken ct = default);

Do not add XML docs to:

  • Private or internal members (unless it's a library's InternalsVisibleTo API)
  • Self-evident members (e.g., public string Name { get; })
  • Test methods

Avoid Reflection-Based Metaprogramming

See Anti-Patterns for detailed guidance on:

  • Why to avoid AutoMapper, Mapster, and similar reflection-based libraries
  • Using explicit mapping methods instead
  • UnsafeAccessorAttribute for legitimate reflection needs

Anti-Patterns to Avoid

See Anti-Patterns for detailed guidance on:

  • Mutable DTOs
  • Classes for value objects
  • Deep inheritance hierarchies
  • Exposing mutable collections
  • Forgetting CancellationToken
  • Blocking on async code

Code Organization

csharp
namespace MyApp.Domain.Orders;

public record Order(
    OrderId Id,
    CustomerId CustomerId,
    Money Total,
    OrderStatus Status,
    IReadOnlyList<OrderItem> Items
)
{
    public bool IsCompleted => Status is OrderStatus.Completed;

    public Result<Order, OrderError> AddItem(OrderItem item)
    {
        if (Status is not OrderStatus.Draft)
            return Result<Order, OrderError>.Failure(
                new OrderError("ORDER_NOT_DRAFT", "Can only add items to draft orders"));

        var newItems = Items.Append(item).ToList();
        var newTotal = new Money(
            Items.Sum(i => i.Total.Amount) + item.Total.Amount,
            Total.Currency);

        return Result<Order, OrderError>.Success(
            this with { Items = newItems, Total = newTotal });
    }
}

public enum OrderStatus
{
    Draft,
    Submitted,
    Processing,
    Completed,
    Cancelled
}

public record OrderItem(
    ProductId ProductId,
    Quantity Quantity,
    Money UnitPrice
)
{
    public Money Total => new(
        UnitPrice.Amount * Quantity.Value,
        UnitPrice.Currency);
}

public readonly record struct OrderId(Guid Value)
{
    public static OrderId New() => new(Guid.NewGuid());
}

public readonly record struct OrderError(string Code, string Message);

Analyzer Enforcement

Configure these analyzers in Directory.Build.props or .editorconfig to enforce standards automatically:

xml
<PropertyGroup>
  <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
  <AnalysisLevel>latest-all</AnalysisLevel>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

Key .editorconfig rules for C# style:

ini
[*.cs]
csharp_style_namespace_declarations = file_scoped:warning
csharp_prefer_braces = true:warning
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
dotnet_style_require_accessibility_modifiers = always:warning
csharp_style_prefer_pattern_matching = true:suggestion

Best Practices Summary

DO's

  • Use record for DTOs, messages, and domain entities
  • Use readonly record struct for value objects
  • Leverage pattern matching with switch expressions
  • Enable and respect nullable reference types
  • Use async/await for all I/O operations
  • Accept CancellationToken in all async methods
  • Use Span<T> and Memory<T> for high-performance scenarios
  • Accept abstractions (IEnumerable<T>, IReadOnlyList<T>)
  • Return appropriate interfaces or concrete types
  • Use Result<T, TError> for expected errors
  • Use ConfigureAwait(false) in library code
  • Pool buffers with ArrayPool<T> for large allocations
  • Prefer composition over inheritance
  • Avoid abstract base classes in application code

DON'Ts

  • Don't use mutable classes when records work
  • Don't use classes for value objects (use readonly record struct)
  • Don't create deep inheritance hierarchies
  • Don't ignore nullable reference type warnings
  • Don't block on async code (.Result, .Wait())
  • Don't use byte[] when Span<byte> suffices
  • Don't forget CancellationToken parameters
  • Don't return mutable collections from APIs
  • Don't throw exceptions for expected business errors
  • Don't use string concatenation in loops
  • Don't allocate large arrays repeatedly (use ArrayPool)

Knowledge Sources

Conventions in this skill are grounded in publicly available content from:

  • Microsoft Framework Design Guidelines -- The canonical reference for .NET naming, type design, and API surface conventions. Source: https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/
  • C# Language Design Notes (Mads Torgersen et al.) -- Design rationale behind C# language features that affect coding standards. Key decisions relevant to this skill: file-scoped namespaces (reducing nesting for readability), pattern matching over type checks (expressiveness), required members (compile-time initialization safety), and var usage guidelines (readability-first). Source: https://github.com/dotnet/csharplang/tree/main/meetings

Additional Resources

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Modern Csharp Coding Standards AI skill do?

Write modern, high-performance C# code using records, pattern matching, value objects, async/await, Span<T>/Memory<T>, and best-practice API design patterns. Emphasizes functional-style programming with C# 12+ features. Use when writing new C# code or refactoring existing code, designing public APIs for libraries or services, optimizing performance-critical code paths, or building async/await-heavy applications.

Why use Modern Csharp Coding Standards on TypingMind?

Because you install it once and use it with any model. Modern Csharp Coding Standards is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Modern Csharp Coding Standards in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/wshaddix/dotnet-skills/tree/master/skills/csharp-coding-standards. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Modern Csharp Coding Standards?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Modern Csharp Coding Standards?

As many as you like. As long as a model supports skills, you can use Modern Csharp Coding Standards with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Modern Csharp Coding Standards AI skill free?

It is published on GitHub by wshaddix. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇