home / skills / phrazzld / claude-config / csharp-modern

csharp-modern skill

/skills/csharp-modern

This skill helps you apply modern C# practices with .NET 8+, async patterns, and nullable reference types across projects.

npx playbooks add skill phrazzld/claude-config --skill csharp-modern

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
3.2 KB
---
name: csharp-modern
description: |
  Modern C# development with .NET 8+, async patterns, and records. Use when:
  - Writing or reviewing C# code
  - Configuring async/await with ConfigureAwait
  - Using nullable reference types
  - Implementing pattern matching
  - Setting up .NET projects
  Keywords: C#, .NET, async, await, ConfigureAwait, nullable, record,
  pattern matching, xUnit, ValueTask
effort: high
---

# Modern C#

.NET 8+, nullable enabled, async-first, records for data.

## Async Patterns

**Always use `async/await` for I/O. Always pass `CancellationToken`:**
```csharp
public async Task<User?> GetUserAsync(
    int id,
    CancellationToken cancellationToken = default)
{
    using var connection = await _factory
        .CreateConnectionAsync(cancellationToken)
        .ConfigureAwait(false);

    return await connection
        .QuerySingleOrDefaultAsync<User>(sql, cancellationToken: cancellationToken)
        .ConfigureAwait(false);
}
```

**`ConfigureAwait(false)` in library code. Never block on async (`.Result`, `.Wait()`).**

## Nullable Reference Types

**Enable project-wide. Treat warnings as errors:**
```xml
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
```

**Explicit nullability:**
```csharp
// Nullable when user might not exist
Task<User?> GetByIdAsync(int id);

// Non-nullable with exception on not found
Task<User> GetRequiredByIdAsync(int id);

// Handle nulls explicitly
if (user is not null) { ProcessUser(user); }
var name = user?.Name ?? "Anonymous";
```

## Records & Immutability

**Records for DTOs and value types:**
```csharp
// DTO
public record CreateOrderRequest(
    string CustomerId,
    IReadOnlyList<OrderItemDto> Items);

// Domain entity
public record class Order
{
    public string Id { get; init; }
    public OrderStatus Status { get; init; }

    public Order Ship() => this with { Status = OrderStatus.Shipped };
}

// Value type (<16 bytes)
public readonly record struct Money(decimal Amount, string Currency);
```

**Never expose mutable collections. Use `IReadOnlyList<T>`.**

## Pattern Matching

**Switch expressions over if-else chains:**
```csharp
public decimal CalculateDiscount(object discount) => discount switch
{
    decimal amount => amount,
    int percentage => percentage / 100m,
    string code => GetDiscountForCode(code),
    _ => throw new ArgumentException("Unsupported type")
};

public string GetShipping(Order order) => order switch
{
    { TotalAmount: > 100, Customer.IsPremium: true } => "Free Express",
    { TotalAmount: > 100 } => "Free Standard",
    _ => "Standard"
};
```

## Project Setup

```xml
<!-- Directory.Build.props -->
<PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <LangVersion>12.0</LangVersion>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <EnableNETAnalyzers>true</EnableNETAnalyzers>
    <AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
```

## Anti-Patterns

- Blocking on async (`.Result`, `.Wait()`)
- `async void` outside event handlers
- Missing `ConfigureAwait(false)` in libraries
- `null!` without documented justification
- Mutable DTOs with public setters
- Switch statements over switch expressions
- Legacy .csproj or packages.config

Overview

This skill codifies modern C# development practices for .NET 8+ focused on async-first code, nullable reference types, records, and expressive pattern matching. It helps you write safer, more maintainable libraries and applications by enforcing recommended project settings and runtime patterns. Use it when creating or reviewing C# codebases that target contemporary .NET and follow async/immutable-first design.

How this skill works

The skill inspects code and project configuration for modern conventions: async/await usage with CancellationToken, ConfigureAwait usage in libraries, nullable reference type settings, record/immutable DTO patterns, and switch expressions. It flags anti-patterns like blocking on async, async void outside handlers, mutable DTOs, and legacy project formats, and suggests fixes and project property recommendations.

When to use it

  • When writing or reviewing C# code targeting .NET 8+
  • When implementing or auditing async APIs (Task/ValueTask) and CancellationToken usage
  • When enabling and enforcing nullable reference types across a project
  • When designing DTOs, domain value types, and preferring records/immutable patterns
  • When modernizing project files and analyzers for current .NET tooling

Best practices

  • Always use async/await for I/O and accept a CancellationToken parameter
  • Use ConfigureAwait(false) in library code and never block on async results (.Result/.Wait())
  • Enable <Nullable>enable</Nullable> and treat warnings as errors in CI
  • Prefer records and readonly record structs for DTOs and small value types
  • Avoid exposing mutable collections; use IReadOnlyList<T> or IReadOnlyCollection<T>
  • Use switch expressions and property pattern matching for clearer intent

Example use cases

  • Implementing a data access method that accepts CancellationToken and uses ConfigureAwait(false)
  • Refactoring mutable DTOs into record types and replacing public setters with init-only
  • Enabling project-wide nullable reference checks and promoting explicit null handling
  • Replacing long if/else chains with switch expressions and property patterns
  • Updating Directory.Build.props to target net8.0, LangVersion 12, and enable analyzers

FAQ

Should ConfigureAwait(false) be used everywhere?

Use ConfigureAwait(false) in library code to avoid capturing SynchronizationContext; in application code (UI, ASP.NET handlers) prefer the default to preserve context when needed.

When to use ValueTask instead of Task?

Use ValueTask for very hot, frequently awaited paths where allocation avoidance matters and the method often completes synchronously; prefer Task for general-purpose async methods.