Skip to content
Development
Skill

/csharp-api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

From plugin
dotnet-skills
1.1k36 skills6 agents
Install
$ npx -y skills add aaronontheweb/dotnet-skills --skill csharp-api-design --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/csharp-api-design

Context preview

The summary Claude sees to decide when to auto-load this skill.

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

SKILL.md

csharp-api-design.SKILL.md
name: api-design
description: Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.
invocable: false

Public API Design and Compatibility

When to Use This Skill

Use this skill when:

  • Designing public APIs for NuGet packages or libraries
  • Making changes to existing public APIs
  • Planning wire format changes for distributed systems
  • Implementing versioning strategies
  • Reviewing pull requests for breaking changes

---

The Three Types of Compatibility

| Type | Definition | Scope | |------|------------|-------| | **API/Source** | Code compiles against newer version | Public method signatures, types | | **Binary** | Compiled code runs against newer version | Assembly layout, method tokens | | **Wire** | Serialized data readable by other versions | Network protocols, persistence formats |

Breaking any of these creates upgrade friction for users.

---

Extend-Only Design

The foundation of stable APIs: **never remove or modify, only extend**.

Three Pillars

1. **Previous functionality is immutable** - Once released, behavior and signatures are locked 2. **New functionality through new constructs** - Add overloads, new types, opt-in features 3. **Removal only after deprecation period** - Years, not releases

Benefits

  • Old code continues working in new versions
  • New and old pathways coexist
  • Upgrades are non-breaking by default
  • Users upgrade on their schedule

**Resources:**

  • [Extend-Only Design](https://aaronstannard.com/extend-only-design/)
  • [OSS Compatibility Standards](https://aaronstannard.com/oss-compatibility-standards/)

---

API Change Guidelines

Safe Changes (Any Release)

// SAFE: Add NEW overload methods that delegate to existing methods
// Existing method - do not modify its signature
public void Process(Order order) { ... }
// New overload - safe to add
public void Process(Order order, CancellationToken ct)
{
    // implementation that handles cancellation
}

// SAFE: Add NEW overloads for additional functionality
// Existing method - do not modify
public void Send(Message msg) { ... }
// New overload - safe to add
public void Send(Message msg, Priority priority)
{
    // implementation that handles priority
}

// ADD new types, interfaces, enums
public interface IOrderValidator { }
public enum OrderStatus { Pending, Complete, Cancelled }

// ADD new members to existing types
public class Order
{
    public DateTimeOffset? ShippedAt { get; init; }  // NEW
}

Unsafe Changes (Never or Major Version Only)

// REMOVE or RENAME public members
public void ProcessOrder(Order order);  // Was: Process()

// CHANGE parameter types or order
public void Process(int orderId);  // Was: Process(Order order)

// CHANGE return types
public Order? GetOrder(string id);  // Was: public Order GetOrder()

// CHANGE access modifiers
internal class OrderProcessor { }  // Was: public

// ADD optional parameters to EXISTING methods (binary incompatible!)
// The compiled IL method signature changes - callers compiled against
// the old signature will get MissingMethodException at runtime.
// Optional parameter defaults are baked into the CALLER's assembly at compile time.
public void Process(Order order, CancellationToken ct = default);  // Breaks binary compat!
public void Send(Message msg, Priority priority = Priority.Normal);  // Breaks binary compat!
// Correct approach: add a NEW overload method instead (see Safe Changes above)

// ADD required parameters without defaults
public void Process(Order order, ILogger logger);  // Breaks callers!

Deprecation Pattern

// Step 1: Mark as obsolete with version (any release)
[Obsolete("Obsolete since v1.5.0. Use ProcessAsync instead.")]
public void Process(Order order) { }

// Step 2: Add new recommended API (same release)
public Task ProcessAsync(Order order, CancellationToken ct = default);

// Step 3: Remove in next major version (v2.0+)
// Only after users have had time to migrate

---

API Approval Testing

Prevent accidental breaking changes with automated API surface testing.

Using ApiApprover + Verify

dotnet add package PublicApiGenerator
dotnet add package Verify.Xunit
[Fact]
public Task ApprovePublicApi()
{
    var api = typeof(MyLibrary.PublicClass).Assembly.GeneratePublicApi();
    return Verify(api);
}

Creates `ApprovePublicApi.verified.txt`:

namespace MyLibrary
{
    public class OrderProcessor
    {
        public OrderProcessor() { }
        public void Process(Order order) { }
        public Task ProcessAsync(Order order, CancellationToken ct = default) { }
    }
}

**Any API change fails the test** - reviewer must explicitly approve changes.

PR Review Process

1. PR includes changes to `*.verified.txt` files 2. Reviewers see exact API surface changes in diff 3. Breaking changes are immediately visible 4. Conscious decision required to approve

---

Wire Compatibility

For distributed systems, serialized data must be readable across versions.

Requirements

| Direction | Requirement | |-----------|-------------| | **Backward** | Old writers → New readers (current version reads old data) | | **Forward** | New writers → Old readers (old version reads new data) |

Both are required for zero-downtime rolling upgrades.

Safely Evolving Wire Formats

**Phase 1: Add read-side support (opt-in)**

// New message type - readers deployed first
public sealed record HeartbeatV2(
    Address From,
    long SequenceNr,
    long CreationTimeMs);  // NEW field

// Deserializer handles both old and new
public object Deserialize(byte[] data, string manifest) => manifest switch
{
    "Heartbeat" => DeserializeHeartbeatV1(data),   // Old format
    "HeartbeatV2" => DeserializeHeartbeatV2(data), // New format
    _ => throw new NotSupportedException()
};

**Phase

Read more
Ships withdotnet-skills

A comprehensive AI coding plugin with 30 skills and 5 specialized agents for professional .NET development. Battle-tested patterns from production systems covering C#, Akka.NET, Aspire, EF Core, testing, and performance optimization.

Get the whole plugin