From dotnet-skills
Design 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 packages or libraries, making changes to existing public APIs, planning wire format changes for distributed systems, or reviewing pull requests for breaking changes.
npx claudepluginhub wshaddix/dotnet-skillsThis skill uses the workspace's default tool permissions.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Use this skill when:
| 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.
The foundation of stable APIs: never remove or modify, only extend.
| 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 |
| 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() |
// WRONG -- abbreviations in public surface
public IReadOnlyList<TxnResult> GetRecentTxns(int cnt);
// CORRECT -- spelled out for clarity
public IReadOnlyList<TransactionResult> GetRecentTransactions(int count);
Consistent parameter ordering reduces cognitive load.
public Task<Widget> GetWidgetAsync(
int widgetId, // 1. Target
WidgetOptions options, // 2. Required
bool includeHistory = false, // 3. Optional
CancellationToken cancellationToken = default); // 4. Always last
// 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);
| 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 |
// 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);
public bool TryGetWidget(int widgetId, [NotNullWhen(true)] out Widget? widget);
public Task<Widget?> TryGetWidgetAsync(int widgetId,
CancellationToken cancellationToken = default);
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;
}
| 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 |
public Widget CreateWidget(string name, decimal price)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(price);
return new Widget(name, price);
}
// 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
}
// 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!
// 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
// 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; }
}
| 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 |
For distributed systems, serialized data must be readable across versions.
| Direction | Requirement |
|---|---|
| Backward | Old writers → New readers |
| Forward | New writers → Old readers |
Both are required for zero-downtime rolling upgrades.
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
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; }
}
// 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
}
Prevent accidental breaking changes with automated API surface testing.
[Fact]
public Task ApprovePublicApi()
{
var api = typeof(MyLibrary.PublicClass).Assembly.GeneratePublicApi();
return Verify(api);
}
*.verified.txt files| 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 |
[Obsolete] for at least one minor version[Obsolete] instead).verified.txt changes reviewed)// "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) { }
// 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)
// AVOID: Type names in wire format
{ "$type": "MyApp.Order, MyApp", "Id": 123 }
// PREFER: Explicit discriminators
{ "type": "order", "id": 123 }
IReadOnlyList<T>.[JsonPropertyName] annotations.async void in API surface -- return Task or ValueTask.System namespace.