Install
$ agentstack add skill-aaronontheweb-dotnet-skills-csharp-api-design ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
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
Resources:
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
- PR includes changes to
*.verified.txtfiles - Reviewers see exact API surface changes in diff
- Breaking changes are immediately visible
- 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 2: Enable write-side (opt-out, next minor version)
// Config to enable new format (off by default initially)
akka.cluster.use-heartbeat-v2 = on
Phase 3: Make default (future version)
After install base has absorbed read-side code.
Schema-Based Serialization
Prefer schema-based formats over reflection-based:
| Format | Type | Wire Compatibility | |--------|------|-------------------| | Protocol Buffers | Schema-based | Excellent - explicit field numbers | | MessagePack | Schema-based | Good - with contracts | | System.Text.Json | Schema-based (with source gen) | Good - explicit properties | | Newtonsoft.Json | Reflection-based | Poor - type names in payload | | BinaryFormatter | Reflection-based | Terrible - never use |
See dotnet/serialization skill for details.
Encapsulation Patterns
Internal APIs
Mark non-public APIs explicitly:
// Attribute for documentation
[InternalApi]
public class ActorSystemImpl { }
// Namespace convention
namespace MyLibrary.Internal
{
public class InternalHelper { } // Public for extensibility, not for users
}
Document clearly:
> Types in .Internal namespaces or marked with [InternalApi] may change between any releases without notice.
Sealing Classes
// DO: Seal classes not designed for inheritance
public sealed class OrderProcessor { }
// DON'T: Leave unsealed by accident
public class OrderProcessor { } // Users might inherit, blocking changes
Interface Segregation
// DO: Small, focused interfaces
public interface IOrderReader
{
Order? GetById(OrderId id);
}
public interface IOrderWriter
{
Task SaveAsync(Order order);
}
// DON'T: Monolithic interfaces (can't add methods without breaking)
public interface IOrderRepository
{
Order? GetById(OrderId id);
Task SaveAsync(Order order);
// Adding new methods breaks all implementations!
}
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 and planned
- 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
Chesterton's Fence
> Before removing or changing something, understand why it exists.
Assume every public API is used by someone. If you want to change it:
- Socialize the proposal on GitHub
- Document migration path
- Provide deprecation period
- Ship in planned release
Pull Request Checklist
When reviewing PRs that touch public APIs:
- [ ] No removed public members (use
[Obsolete]instead) - [ ] No changed signatures (add overloads instead)
- [ ] No new required parameters (use defaults)
- [ ] API approval test updated (
.verified.txtchanges 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 GetOrderAsync(OrderId id) // Was sync!
{
// "Fixed" to be async - but breaks all callers
}
// Correct: Add new method, deprecate old
[Obsolete("Use GetOrderAsync instead")]
public Order GetOrder(OrderId id) => GetOrderAsync(id).Result;
public async Task GetOrderAsync(OrderId id) { }
Silent Behavior Changes
// Changing defaults breaks users who relied on old behavior
public void Configure(bool enableCaching = true) // Was: false!
// Correct: New parameter with new name
public void Configure(
bool enableCaching = false, // Original default preserved
bool enableNewCaching = true) // New behavior opt-in
Polymorphic Serialization
// AVOID: Type names in wire format
{ "$type": "MyApp.Order, MyApp", "Id": 123 }
// Renaming Order class = wire break!
// PREFER: Explicit discriminators
{ "type": "order", "id": 123 }
Resources
- Making Public API Changes
- Wire Format Changes
- Extend-Only Design
- OSS Compatibility Standards
- Semantic Versioning
- PublicApiGenerator
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Aaronontheweb
- Source: Aaronontheweb/dotnet-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.