api-design

Public API Design and Compatibility

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "api-design" with this command: npx skills add wshaddix/dotnet-skills/wshaddix-dotnet-skills-api-design

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

  • Previous functionality is immutable - Once released, behavior and signatures are locked

  • New functionality through new constructs - Add overloads, new types, opt-in features

  • 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

Naming Conventions for API Surface

Type Naming

Type Kind Suffix Pattern Example

Base class Base suffix only for abstract base types ValidatorBase

Interface I prefix IWidgetFactory

Exception Exception suffix WidgetNotFoundException

Attribute Attribute suffix RequiredPermissionAttribute

Event args EventArgs suffix WidgetCreatedEventArgs

Options/config Options suffix WidgetServiceOptions

Builder Builder suffix WidgetBuilder

Method Naming

Pattern Convention Example

Synchronous Verb or verb phrase Calculate() , GetWidget()

Asynchronous Async suffix CalculateAsync() , GetWidgetAsync()

Boolean query Is /Has /Can prefix IsValid() , HasPermission()

Try pattern Try prefix, out parameter TryGetWidget(int id, out Widget widget)

Factory Create prefix CreateWidget() , CreateWidgetAsync()

Conversion To /From prefix ToDto() , FromEntity()

Avoid Abbreviations in Public API

// WRONG -- abbreviations in public surface public IReadOnlyList<TxnResult> GetRecentTxns(int cnt);

// CORRECT -- spelled out for clarity public IReadOnlyList<TransactionResult> GetRecentTransactions(int count);

Parameter Ordering

Consistent parameter ordering reduces cognitive load.

Standard Order

  • Target/subject -- the primary entity being operated on

  • Required parameters -- essential inputs without defaults

  • Optional parameters -- inputs with sensible defaults

  • Cancellation token -- always last (convention enforced by CA1068)

public Task<Widget> GetWidgetAsync( int widgetId, // 1. Target WidgetOptions options, // 2. Required bool includeHistory = false, // 3. Optional CancellationToken cancellationToken = default); // 4. Always last

Overload Progression

// Simple -- sensible defaults public Task<Widget> GetWidgetAsync(int widgetId, CancellationToken cancellationToken = default) => GetWidgetAsync(widgetId, WidgetOptions.Default, cancellationToken);

// Detailed -- full control public Task<Widget> GetWidgetAsync(int widgetId, WidgetOptions options, CancellationToken cancellationToken = default);

Return Type Selection

When to Return What

Scenario Return Type Rationale

Single entity, always exists Widget

Throw if not found

Single entity, may not exist Widget?

Nullable communicates optionality

Collection, possibly empty IReadOnlyList<Widget>

Immutable, indexable, communicates no mutation

Streaming results IAsyncEnumerable<Widget>

Avoids buffering entire result set

Operation result with detail Result<Widget> / discriminated union Rich error info without exceptions

Void with async Task

Never async void except event handlers

Frequently synchronous completion ValueTask<Widget>

Avoids Task allocation on cache hits

Prefer IReadOnlyList Over IEnumerable

// WRONG -- caller does not know if result is materialized or lazy public IEnumerable<Widget> GetWidgets();

// CORRECT -- signals materialized, indexable collection public IReadOnlyList<Widget> GetWidgets();

// CORRECT -- signals streaming/lazy evaluation explicitly public IAsyncEnumerable<Widget> GetWidgetsStreamAsync( CancellationToken cancellationToken = default);

The Try Pattern

public bool TryGetWidget(int widgetId, [NotNullWhen(true)] out Widget? widget);

public Task<Widget?> TryGetWidgetAsync(int widgetId, CancellationToken cancellationToken = default);

Error Reporting Strategies

Exception Hierarchy

public class WidgetServiceException : Exception { public WidgetServiceException(string message) : base(message) { } public WidgetServiceException(string message, Exception inner) : base(message, inner) { } }

public class WidgetNotFoundException : WidgetServiceException { public int WidgetId { get; } public WidgetNotFoundException(int widgetId) : base($"Widget {widgetId} not found.") => WidgetId = widgetId; }

public class WidgetValidationException : WidgetServiceException { public IReadOnlyList<string> Errors { get; } public WidgetValidationException(IReadOnlyList<string> errors) : base("Widget validation failed.") => Errors = errors; }

When to Use Exceptions vs Return Values

Approach When to Use

Throw exception Unexpected failures, programming errors, infrastructure failures

Return null / default

"Not found" is a normal, expected outcome

Try pattern (bool

  • out ) Parsing or validation where failure is common and synchronous

Result object Multiple failure modes that callers need to distinguish

Argument Validation

public Widget CreateWidget(string name, decimal price) { ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(price);

return new Widget(name, price);

}

API Change Guidelines

Safe Changes (Any Release)

// ADD new overloads with default parameters public void Process(Order order, CancellationToken ct = default);

// ADD new optional parameters to existing methods public void Send(Message msg, Priority priority = Priority.Normal);

// 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 required parameters without defaults public void Process(Order order, ILogger logger); // Breaks callers!

Deprecation Pattern

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

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

// Step 3: Remove in next major version

Extension Points

Interface-Based Extension

// GOOD -- interface-based extension point public interface IWidgetValidator { ValueTask<bool> ValidateAsync(Widget widget, CancellationToken ct = default); }

// GOOD -- delegate-based extension for simple hooks public class WidgetServiceOptions { public Func<Widget, CancellationToken, ValueTask>? OnWidgetCreated { get; set; } }

Extension Method Guidelines

Guideline Rationale

Place extensions in the same namespace as the type Discoverable without extra using statements

Never put extensions in System or System.Linq

Namespace pollution

Prefer instance methods over extensions when you own the type Extensions are a last resort

Keep the this parameter as the most specific usable type Avoids polluting IntelliSense

Wire Compatibility

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

Requirements

Direction Requirement

Backward Old writers → New readers

Forward New writers → Old readers

Both are required for zero-downtime rolling upgrades.

Safely Evolving Wire Formats

Phase 1: Add read-side support

public sealed record HeartbeatV2( Address From, long SequenceNr, long CreationTimeMs); // NEW field

public object Deserialize(byte[] data, string manifest) => manifest switch { "Heartbeat" => DeserializeHeartbeatV1(data), "HeartbeatV2" => DeserializeHeartbeatV2(data), _ => throw new NotSupportedException() };

Phase 2: Enable write-side (next minor version)

akka.cluster.use-heartbeat-v2 = on

Defensive Serialization Design

public sealed class WidgetDto { [JsonPropertyName("id")] public int Id { get; init; }

[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("category")]
public string? Category { get; init; }

[JsonPropertyName("priority")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int Priority { get; init; }

}

Enum Serialization Strategy

// GOOD -- string serialization is rename-safe [JsonConverter(typeof(JsonStringEnumConverter))] public enum WidgetStatus { Draft, Active, Archived }

// RISKY -- integer serialization breaks when members are reordered public enum WidgetPriority { Low = 0, Medium = 1, High = 2 }

API Approval Testing

Prevent accidental breaking changes with automated API surface testing.

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

PR Review Process

  • PR includes changes to *.verified.txt files

  • Reviewers see exact API surface changes in diff

  • Breaking changes are immediately visible

  • Conscious decision required to approve

Versioning Strategy

Semantic Versioning (Practical)

Version Changes Allowed

Patch (1.0.x) Bug fixes, security patches

Minor (1.x.0) New features, deprecations, obsolete removal

Major (x.0.0) Breaking changes, old API removal

Key Principles

  • No surprise breaks - Even major versions should be announced

  • Extensions anytime - New APIs can ship in any release

  • Deprecate before remove - [Obsolete] for at least one minor version

  • Communicate timelines - Users need to plan upgrades

Pull Request Checklist

  • No removed public members (use [Obsolete] instead)

  • No changed signatures (add overloads instead)

  • No new required parameters (use defaults)

  • API approval test updated (.verified.txt changes reviewed)

  • Wire format changes are opt-in (read-side first)

  • Breaking changes documented (release notes, migration guide)

Anti-Patterns

Breaking Changes Disguised as Fixes

// "Bug fix" that breaks users public async Task<Order> GetOrderAsync(OrderId id) // Was sync! { }

// Correct: Add new method, deprecate old [Obsolete("Use GetOrderAsync instead")] public Order GetOrder(OrderId id) => GetOrderAsync(id).Result;

public async Task<Order> GetOrderAsync(OrderId id) { }

Silent Behavior Changes

// Changing defaults breaks users public void Configure(bool enableCaching = true) // Was: false!

// Correct: New parameter with new name public void Configure( bool enableCaching = false, bool enableNewCaching = true)

Polymorphic Serialization

// AVOID: Type names in wire format { "$type": "MyApp.Order, MyApp", "Id": 123 }

// PREFER: Explicit discriminators { "type": "order", "id": 123 }

Agent Gotchas

  • Do not use abbreviations in public API names -- spell out words.

  • Do not place CancellationToken before optional parameters -- CA1068 enforces last.

  • Do not return mutable collections from public APIs -- return IReadOnlyList<T> .

  • Do not change serialized property names without [JsonPropertyName] annotations.

  • Do not add required parameters to existing public methods -- add overload or use defaults.

  • Do not use async void in API surface -- return Task or ValueTask .

  • Do not design exception hierarchies without a base library exception.

  • Do not put extension methods in the System namespace.

Resources

  • Making Public API Changes

  • Wire Format Changes

  • Extend-Only Design

  • OSS Compatibility Standards

  • Semantic Versioning

  • PublicApiGenerator

  • Framework Design Guidelines

  • Breaking changes reference

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

api-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-performance-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-solid-principles

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-winforms-basics

No summary provided by upstream source.

Repository SourceNeeds Review