public IReadOnlyList<TxnResult> GetRecentTxns(int cnt);
public IReadOnlyList<TransactionResult> GetRecentTransactions(int count);
```text
---
## Parameter Ordering
Consistent parameter ordering reduces cognitive load and enables fluent usage patterns across an API surface.
### Standard Order
1. **Target/subject** -- the primary entity being operated on
2. **Required parameters** -- essential inputs without defaults
3. **Optional parameters** -- inputs with sensible defaults
4. **Cancellation token** -- always last (convention enforced by CA1068)
```csharp
public Task<Widget> GetWidgetAsync(
int widgetId, // 1. Target
WidgetOptions options, // 2. Required
bool includeHistory = false, // 3. Optional
CancellationToken cancellationToken = default);
public Task<Widget> UpdateWidgetAsync(
int widgetId, // 1. Target
WidgetUpdateRequest request, // 2. Required
bool validateFirst = true, // 3. Optional
CancellationToken cancellationToken = default);
```text
### Overload Progression
Design overloads as a progression from simple to detailed. Each overload should delegate to the next more specific one:
```csharp
public Task<Widget> GetWidgetAsync(int widgetId,
CancellationToken cancellationToken = default)
=> GetWidgetAsync(widgetId, WidgetOptions.Default, cancellationToken);
public Task<Widget> GetWidgetAsync(int widgetId,
WidgetOptions options,
CancellationToken cancellationToken = default);
```text
---
## 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
```csharp
public IEnumerable<Widget> GetWidgets();
public IReadOnlyList<Widget> GetWidgets();
public IAsyncEnumerable<Widget> GetWidgetsStreamAsync(
CancellationToken cancellationToken = default);
```text
### The Try Pattern
Use the Try pattern for operations that have a common, non-exceptional failure mode:
```csharp
public bool TryGetWidget(int widgetId, [NotNullWhen(true)] out Widget? widget);
public Task<Widget?> TryGetWidgetAsync(int widgetId,
CancellationToken cancellationToken = default);
```text
---
## Error Reporting Strategies
### Exception Hierarchy
Design exception types that enable callers to catch at the right granularity:
```csharp
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;
}
```text
### 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:
```csharp
public Widget CreateWidget(string name, decimal price)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(price);
return new Widget(name, price);
}
```text
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:
```csharp
public interface IWidgetValidator
{
ValueTask<bool> ValidateAsync(Widget widget, CancellationToken ct = default);
}
public class WidgetServiceOptions
{
public Func<Widget, CancellationToken, ValueTask>? OnWidgetCreated { get; set; }
}
public sealed class WidgetServiceBuilder
{
private readonly List<IWidgetValidator> _validators = [];
public WidgetServiceBuilder AddValidator(IWidgetValidator validator)
{
_validators.Add(validator);
return this;
}
public WidgetServiceBuilder AddValidator(
Func<Widget, CancellationToken, ValueTask<bool>> validator)
{
_validators.Add(new DelegateValidator(validator));
return this;
}
public WidgetService Build() => new(_validators);
}
```text
### 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
```csharp
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; }
}
```json
### Enum Serialization Strategy
```csharp
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum WidgetStatus
{
Draft,
Active,
Archived
}
public enum WidgetPriority
{
Low = 0,
Medium = 1,
High = 2
}
```text
---
## API Design Checklist
Before shipping a new public API, verify each concern:
1. **Naming** -- follows .NET naming conventions, no abbreviations, consistent with rest of API surface
2. **Parameters** -- ordered (target, required, optional, CancellationToken), no more than ~5 parameters (use options
object for complex APIs)
3. **Return types** -- appropriate for the scenario (nullable for optional, IReadOnlyList for collections,
Task/ValueTask for async)
4. **Error handling** -- clear exception types, argument validation at entry points, Try pattern where failure is
expected
5. **Extension points** -- interfaces or delegates, not virtual methods on concrete classes
6. **Wire safety** -- serialized types use explicit property names, additive-only evolution, enum strategy documented
7. **Compatibility** -- changes reviewed against [skill:dotnet-library-api-compat] rules before release
---
## Agent Gotchas
1. **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.
2. **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.
3. **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.
4. **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.
5. **Do parameters to existing methods** -- a source-breaking change. Add a overload
use optional parameters defaults instead.
6. **Do use ` ` API surface** -- `Task` `ValueTask`. The only valid ` ` framework
handlers. See [skill:dotnet-csharp--patterns].
7. **Do design exception hierarchies without a library exception** -- callers need a single point all
library errors. Always provide a exception type that specific exceptions derive .
8. **Do put extension methods the `System` ** -- pollution affects every every
consumer project. Use the library's own a dedicated `.Extensions` sub-.
---
## Prerequisites
- .NET 8.0+ SDK
- Familiarity C# naming ()
- Understanding of binary/source compatibility ()
- System.Text.Json wire compatibility examples
---
## Code ()
**Primary approach:** Use Serena symbol operations efficient code navigation:
1. **Find definitions**: `serena_find_symbol` instead of text search
2. **Understand structure**: `serena_get_symbols_overview` organization
3. **Track references**: `serena_find_referencing_symbols` impact analysis
4. **Precise edits**: `serena_replace_symbol_body` clean modifications
**When to use Serena vs traditional tools:**
- ✅ **Use Serena**: Navigation, refactoring, dependency analysis, precise edits
- ✅ **Use Read/Grep**: Reading full files, pattern matching, simple text operations
- ✅ **Fallback**: If Serena unavailable, traditional tools work fine
**Example workflow:**
```text
# Instead of:
Read: src/Services/OrderService.cs
Grep: " ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"
```
## References
- [Framework Design ()]()
- [API design best ()]()
- [Breaking changes reference]()
- [System.Text.Json serialization]()
- [CA1068: CancellationToken parameters must come last]()