$npx -y skills add wshaddix/dotnet-skills --skill csharp-api-designDesign stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, versioning, naming conventions, parameter ordering, and return types for NuGet packages and distributed systems. Use when designing public APIs for NuGet packa
| 1 | # Public API Design and Compatibility |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | Use this skill when: |
| 6 | - Designing public APIs for NuGet packages or libraries |
| 7 | - Making changes to existing public APIs |
| 8 | - Planning wire format changes for distributed systems |
| 9 | - Implementing versioning strategies |
| 10 | - Reviewing pull requests for breaking changes |
| 11 | |
| 12 | --- |
| 13 | |
| 14 | ## The Three Types of Compatibility |
| 15 | |
| 16 | | Type | Definition | Scope | |
| 17 | |------|------------|-------| |
| 18 | | **API/Source** | Code compiles against newer version | Public method signatures, types | |
| 19 | | **Binary** | Compiled code runs against newer version | Assembly layout, method tokens | |
| 20 | | **Wire** | Serialized data readable by other versions | Network protocols, persistence formats | |
| 21 | |
| 22 | Breaking any of these creates upgrade friction for users. |
| 23 | |
| 24 | --- |
| 25 | |
| 26 | ## Extend-Only Design |
| 27 | |
| 28 | The foundation of stable APIs: **never remove or modify, only extend**. |
| 29 | |
| 30 | ### Three Pillars |
| 31 | |
| 32 | 1. **Previous functionality is immutable** - Once released, behavior and signatures are locked |
| 33 | 2. **New functionality through new constructs** - Add overloads, new types, opt-in features |
| 34 | 3. **Removal only after deprecation period** - Years, not releases |
| 35 | |
| 36 | ### Benefits |
| 37 | |
| 38 | - Old code continues working in new versions |
| 39 | - New and old pathways coexist |
| 40 | - Upgrades are non-breaking by default |
| 41 | - Users upgrade on their schedule |
| 42 | |
| 43 | --- |
| 44 | |
| 45 | ## Naming Conventions for API Surface |
| 46 | |
| 47 | ### Type Naming |
| 48 | |
| 49 | | Type Kind | Suffix Pattern | Example | |
| 50 | |-----------|---------------|---------| |
| 51 | | Base class | `Base` suffix only for abstract base types | `ValidatorBase` | |
| 52 | | Interface | `I` prefix | `IWidgetFactory` | |
| 53 | | Exception | `Exception` suffix | `WidgetNotFoundException` | |
| 54 | | Attribute | `Attribute` suffix | `RequiredPermissionAttribute` | |
| 55 | | Event args | `EventArgs` suffix | `WidgetCreatedEventArgs` | |
| 56 | | Options/config | `Options` suffix | `WidgetServiceOptions` | |
| 57 | | Builder | `Builder` suffix | `WidgetBuilder` | |
| 58 | |
| 59 | ### Method Naming |
| 60 | |
| 61 | | Pattern | Convention | Example | |
| 62 | |---------|-----------|---------| |
| 63 | | Synchronous | Verb or verb phrase | `Calculate()`, `GetWidget()` | |
| 64 | | Asynchronous | `Async` suffix | `CalculateAsync()`, `GetWidgetAsync()` | |
| 65 | | Boolean query | `Is`/`Has`/`Can` prefix | `IsValid()`, `HasPermission()` | |
| 66 | | Try pattern | `Try` prefix, `out` parameter | `TryGetWidget(int id, out Widget widget)` | |
| 67 | | Factory | `Create` prefix | `CreateWidget()`, `CreateWidgetAsync()` | |
| 68 | | Conversion | `To`/`From` prefix | `ToDto()`, `FromEntity()` | |
| 69 | |
| 70 | ### Avoid Abbreviations in Public API |
| 71 | |
| 72 | ```csharp |
| 73 | // WRONG -- abbreviations in public surface |
| 74 | public IReadOnlyList<TxnResult> GetRecentTxns(int cnt); |
| 75 | |
| 76 | // CORRECT -- spelled out for clarity |
| 77 | public IReadOnlyList<TransactionResult> GetRecentTransactions(int count); |
| 78 | ``` |
| 79 | |
| 80 | --- |
| 81 | |
| 82 | ## Parameter Ordering |
| 83 | |
| 84 | Consistent parameter ordering reduces cognitive load. |
| 85 | |
| 86 | ### Standard Order |
| 87 | |
| 88 | 1. **Target/subject** -- the primary entity being operated on |
| 89 | 2. **Required parameters** -- essential inputs without defaults |
| 90 | 3. **Optional parameters** -- inputs with sensible defaults |
| 91 | 4. **Cancellation token** -- always last (convention enforced by CA1068) |
| 92 | |
| 93 | ```csharp |
| 94 | public Task<Widget> GetWidgetAsync( |
| 95 | int widgetId, // 1. Target |
| 96 | WidgetOptions options, // 2. Required |
| 97 | bool includeHistory = false, // 3. Optional |
| 98 | CancellationToken cancellationToken = default); // 4. Always last |
| 99 | ``` |
| 100 | |
| 101 | ### Overload Progression |
| 102 | |
| 103 | ```csharp |
| 104 | // Simple -- sensible defaults |
| 105 | public Task<Widget> GetWidgetAsync(int widgetId, |
| 106 | CancellationToken cancellationToken = default) |
| 107 | => GetWidgetAsync(widgetId, WidgetOptions.Default, cancellationToken); |
| 108 | |
| 109 | // Detailed -- full control |
| 110 | public Task<Widget> GetWidgetAsync(int widgetId, |
| 111 | WidgetOptions options, |
| 112 | CancellationToken cancellationToken = default); |
| 113 | ``` |
| 114 | |
| 115 | --- |
| 116 | |
| 117 | ## Return Type Selection |
| 118 | |
| 119 | ### When to Return What |
| 120 | |
| 121 | | Scenario | Return Type | Rationale | |
| 122 | |----------|------------|-----------| |
| 123 | | Single entity, always exists | `Widget` | Throw if not found | |
| 124 | | Single entity, may not exist | `Widget?` | Nullable communicates optionality | |
| 125 | | Collection, possibly empty | `IReadOnlyList<Widget>` | Immutable, indexable, communicates no mutation | |
| 126 | | Streaming results | `IAsyncEnumerable<Widget>` | Avoids buffering entire result set | |
| 127 | | Operation result with detail | `Result<Widget>` / discriminated union | Rich error info without exceptions | |
| 128 | | Void with async | `Task` | Never `async void` except event handlers | |
| 129 | | Frequently synchronous completion | `ValueTask<Widget>` | Avoids Task allocation on cache hits | |
| 130 | |
| 131 | ### Prefe |