dotnet-csharp-api-design

dotnet-csharp-api-design

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 "dotnet-csharp-api-design" with this command: npx skills add novotnyllc/dotnet-artisan/novotnyllc-dotnet-artisan-dotnet-csharp-api-design

dotnet-csharp-api-design

Design-time principles for creating public .NET APIs that are intuitive, consistent, and forward-compatible. Covers naming conventions for API surface, parameter ordering, return type selection, error reporting strategies, extension points, and wire compatibility for serialized types. This skill addresses the design decisions that make APIs compatible and usable in the first place, before enforcement tooling gets involved.

Version assumptions: .NET 8.0+ baseline. Examples use modern C# features (primary constructors, collection expressions) where appropriate.

Scope

  • Naming conventions for public API types, methods, and parameters

  • Parameter ordering and overload progression

  • Return type selection (nullable, IReadOnlyList, IAsyncEnumerable, ValueTask)

  • Error reporting strategies (exceptions, Try pattern, result objects)

  • Extension points (interfaces, delegates, builder patterns)

  • Wire compatibility for serialized types

Out of scope

  • Binary/source compatibility enforcement and tooling -- see [skill:dotnet-library-api-compat]

  • PublicApiAnalyzers, Verify snapshots, and CI validation of API surface -- see [skill:dotnet-api-surface-validation]

  • General C# naming conventions and file layout -- see [skill:dotnet-csharp-coding-standards]

  • HTTP API versioning and URL design -- see [skill:dotnet-api-versioning]

  • NuGet packaging and SemVer mechanics -- see [skill:dotnet-nuget-authoring]

Cross-references: [skill:dotnet-library-api-compat] for compatibility enforcement, [skill:dotnet-api-surface-validation] for CI detection, [skill:dotnet-csharp-coding-standards] for general naming rules, [skill:dotnet-api-versioning] for HTTP API versioning, [skill:dotnet-nuget-authoring] for SemVer and packaging.

Naming Conventions for API Surface

Type Naming

Follow the .NET Framework Design Guidelines naming patterns for public API types:

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

Spell out words in public APIs even if internal code uses abbreviations. Public APIs are consumed by developers who may not share the team's domain shorthand:

// 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 and enables fluent usage patterns across an API surface.

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)

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

public Task<Widget> UpdateWidgetAsync( int widgetId, // 1. Target WidgetUpdateRequest request, // 2. Required bool validateFirst = true, // 3. Optional CancellationToken cancellationToken = default); // 4. Always last

Overload Progression

Design overloads as a progression from simple to detailed. Each overload should delegate to the next more specific one:

// 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 reference type 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 for Materialized Collections

// 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

Use the Try pattern for operations that have a common, non-exceptional failure mode:

// Parsing, lookup, validation -- failure is expected, not exceptional public bool TryGetWidget(int widgetId, [NotNullWhen(true)] out Widget? widget);

// Async Try pattern -- return nullable instead of out parameter public Task<Widget?> TryGetWidgetAsync(int widgetId, CancellationToken cancellationToken = default);

Error Reporting Strategies

Exception Hierarchy

Design exception types that enable callers to catch at the right granularity:

// Base exception for the library -- callers can catch all library errors public class WidgetServiceException : Exception { public WidgetServiceException(string message) : base(message) { } public WidgetServiceException(string message, Exception inner) : base(message, inner) { } }

// Specific exceptions derive from the base 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 (query patterns)

Try pattern (bool

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

Result object Multiple failure modes that callers need to distinguish without try/catch

Argument Validation

Validate public API entry points immediately and throw the standard .NET exceptions:

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

// Proceed with creation
return new Widget(name, price);

}

Use ArgumentException.ThrowIfNullOrWhiteSpace (.NET 8+) and ArgumentOutOfRangeException.ThrowIfNegativeOrZero (.NET 8+) instead of manual null checks with throw new ArgumentNullException(...) . These throw helpers are optimized by the JIT (no delegate allocation, better inlining).

Extension Points

Designing for Extensibility Without Inheritance

Prefer composition and interfaces over class inheritance for extension points:

// 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; } }

// GOOD -- builder pattern for complex configuration public sealed class WidgetServiceBuilder { private readonly List<IWidgetValidator> _validators = [];

public WidgetServiceBuilder AddValidator(IWidgetValidator validator)
{
    _validators.Add(validator);
    return this;
}

public WidgetServiceBuilder AddValidator(
    Func&#x3C;Widget, CancellationToken, ValueTask&#x3C;bool>> validator)
{
    _validators.Add(new DelegateValidator(validator));
    return this;
}

public WidgetService Build() => new(_validators);

}

Extension Method Guidelines

Guideline Rationale

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

Never put extensions in System or System.Linq

Namespace pollution affects all consumers

Prefer instance methods over extensions when you own the type Extensions are a last resort for types you do not own

Keep the extension's this parameter as the most specific usable type IEnumerable<T> not object ; avoids polluting IntelliSense

Wire Compatibility for Serialized Types

Types that are serialized (JSON, Protobuf, MessagePack) or persisted form an implicit contract. Changing their shape breaks existing clients or stored data.

Safe Changes (Wire Compatible)

Change Why Safe

Add optional property with default Old payloads deserialize with default; old clients ignore new field

Add new enum member at the end Existing serialized values map to existing members

Rename property with [JsonPropertyName] annotation Wire name stays the same

Breaking Changes (Wire Incompatible)

Change Impact

Remove or rename property (without annotation) Old payloads lose data; old clients send unrecognized fields

Change property type Deserialization failure or silent data loss

Reorder enum members (for integer-serialized enums) Existing stored integers map to wrong members

Change from class to struct or vice versa Serializer behavior changes (null handling, default values)

Defensive Serialization Design

// Version-tolerant DTO with explicit wire names public sealed class WidgetDto { [JsonPropertyName("id")] public int Id { get; init; }

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

// V2 addition -- optional with default, old payloads work fine
[JsonPropertyName("category")]
public string? Category { get; init; }

// V3 addition -- use JsonIgnoreCondition to exclude defaults from wire
[JsonPropertyName("priority")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int Priority { get; init; }

}

Enum Serialization Strategy

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

// RISKY -- integer serialization breaks when members are reordered or inserted // Only use when wire format size is critical and members are append-only public enum WidgetPriority { Low = 0, Medium = 1, High = 2 // New members MUST go at the end with explicit values }

API Design Checklist

Before shipping a new public API, verify each concern:

  • Naming -- follows .NET naming conventions, no abbreviations, consistent with rest of API surface

  • Parameters -- ordered (target, required, optional, CancellationToken), no more than ~5 parameters (use options object for complex APIs)

  • Return types -- appropriate for the scenario (nullable for optional, IReadOnlyList for collections, Task/ValueTask for async)

  • Error handling -- clear exception types, argument validation at entry points, Try pattern where failure is expected

  • Extension points -- interfaces or delegates, not virtual methods on concrete classes

  • Wire safety -- serialized types use explicit property names, additive-only evolution, enum strategy documented

  • Compatibility -- changes reviewed against [skill:dotnet-library-api-compat] rules before release

Agent Gotchas

  • Do not use abbreviations in public API names -- spell out words even when internal code uses shorthand. Public APIs are consumed by developers outside the team who do not share the domain vocabulary.

  • Do not place CancellationToken before optional parameters -- CA1068 enforces CancellationToken as the last parameter. Placing it earlier breaks the standard ordering convention and triggers analyzer warnings.

  • Do not return mutable collections from public APIs -- return IReadOnlyList<T> or IReadOnlyCollection<T> instead of List<T> or IList<T> . Mutable return types allow callers to corrupt internal state.

  • Do not change serialized property names without [JsonPropertyName] annotations -- renaming a C# property without preserving the wire name breaks all existing serialized data and API clients.

  • Do not add required parameters to existing public methods -- this is a source-breaking change. Add a new overload or use optional parameters with defaults instead.

  • Do not use async void in API surface -- return Task or ValueTask . The only valid async void is framework event handlers. See [skill:dotnet-csharp-async-patterns].

  • Do not design exception hierarchies without a base library exception -- callers need a single catch point for all library errors. Always provide a base exception type that specific exceptions derive from.

  • Do not put extension methods in the System namespace -- namespace pollution affects every file in every consumer project. Use the library's own namespace or a dedicated .Extensions sub-namespace.

Prerequisites

  • .NET 8.0+ SDK

  • Familiarity with C# naming conventions (see [skill:dotnet-csharp-coding-standards])

  • Understanding of binary/source compatibility concepts (see [skill:dotnet-library-api-compat])

  • System.Text.Json for wire compatibility examples

References

  • Framework Design Guidelines (Microsoft Learn)

  • API design best practices (Microsoft REST API Guidelines)

  • Breaking changes reference

  • System.Text.Json serialization

  • CA1068: CancellationToken parameters must come last

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

dotnet-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-csharp

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-api

No summary provided by upstream source.

Repository SourceNeeds Review