API Design, Coding Standards and Library Best Practices

Libraries shared across teams (protocol stacks, test frameworks) must evolve without breaking dozens of consumers.

Concept

Public API Design Names follow.NET conventions PascalCase types/members, camelCase parameters, _camelCase private fields, I prefix for interfaces, Async suffix

Accept general, return specific-but-read-only Accept IEnumerable<T>, return IReadOnlyList<T> Nullable annotations

string? where null is valid; [NotNullWhen(true)] on Try methods

For programming errors (ArgumentNullException.ThrowIfNull) and exceptional conditions; document them (<exception>) Result types / Try-pattern

For expected failures (TryReadVin, Result<T>) — no exceptions for control flow Async API design

Return Task/ValueTask, *Async suffix, CancellationToken cancellationToken = default as the last parameter, no async void Options objects

FlashOptions instead of 8 parameters — new optional properties don't break callers Backward compatibility

Never remove or change public members in a minor version: add overloads, mark old ones [Obsolete] with guidance, remove in the next major version

Semantic versioning (Module 46); API diff tools (Microsoft.DotNet.ApiCompat, package validation) catch breaking changes XML documentation

/// comments on every public member; generated into the package

Formatting and style: .editorconfig at the repository root is the single source of truth for indentation, var usage, namespaces, naming rules and analyzer severities; IDEs and dotnet format apply it.

Analyzers: the .NET SDK includes code-quality (CAxxxx) and code-style (IDExxxx) analyzers; <AnalysisLevel>latest-recommended</AnalysisLevel> enables a sensible set and <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> makes style rules show up in command-line builds. Third-party: StyleCop.Analyzers, SonarAnalyzer, Roslynator.

Static analysis in CI: dotnet build -warnaserror, dotnet format --verify-no-changes, SonarQube/CodeQL.

Documentation: README per project, XML docs for public APIs, architecture decision records (ADRs) for important choices.

Code review: small PRs; review for correctness, tests, readability, security and API compatibility; automate everything a machine can check so humans review design.

Why It Matters

Libraries shared across teams (protocol stacks, test frameworks) must evolve without breaking dozens of consumers. Consistent, analyzer-enforced standards make a large codebase readable by everyone and catch bugs before review.

Syntax

public bool TryReadVin(string ecu, [NotNullWhen(true)] out string? vin);
public ValueTask<string> ReadVinAsync(string ecu, CancellationToken cancellationToken = default);
[Obsolete("Use ReadVinAsync. GetVin will be removed in version 3.0.")] public string GetVin(string ecu);

Example

60.1 API design techniques A caller of the obsolete member gets a compiler warning that points to the replacement:

60.2 .editorconfig and analyzers enforcing standards at build time

Code before and after applying the rules:

using System.Diagnostics.CodeAnalysis;

var client = new DiagnosticClient();

// Try-pattern for expected failures; exceptions for programming errors and exceptional cases
Console.WriteLine(client.TryReadVin("VCU", out string? vin) ? $"TryReadVin: {vin}" : "TryReadVin: failed");
Console.WriteLine(client.TryReadVin("XYZ", out vin) ? $"TryReadVin: {vin}" : "TryReadVin: no answer (expected failure, no exception)");
try { client.ReadVinAsync(null!, CancellationToken.None).GetAwaiter().GetResult(); }
catch (ArgumentNullException ex) { Console.WriteLine($"guard clause: {ex.GetType().Name} ({ex.ParamName})"); }

// Async APIs: *Async suffix, CancellationToken last with a default, ValueTask for hot paths that often complete synchronously
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
try { await client.ReadVinAsync("SLOW", cts.Token); }
catch (OperationCanceledException) { Console.WriteLine("ReadVinAsync honoured the cancellation token"); }

// Backward compatibility: keep the old member, mark it [Obsolete], forward to the new one
#pragma warning disable CS0618
Console.WriteLine($"old API still works: {client.GetVin("VCU")}");
#pragma warning restore CS0618

//
…
var client = new DiagnosticClient();
Console.WriteLine(client.GetVin("VCU"));

public sealed class DiagnosticClient
{
    [Obsolete("Use ReadVinAsync. GetVin will be removed in version 3.0.")]
    public string GetVin(string ecu) => "EHBAUREV250000001";
}
root = true

[*.cs]
indent_style = space
indent_size = 4

# Naming: private fields _camelCase
dotnet_naming_rule.private_fields.symbols = private_fields
dotnet_naming_rule.private_fields.style = underscore_camel
dotnet_naming_rule.private_fields.severity = warning
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.underscore_camel.required_prefix = _
dotnet_naming_style.underscore_camel.capitalization = camel_case

# Style rules enforced at build time
csharp_style_var_for_built_in_types = false:warning
dotnet_diagnostic.IDE0040.severity = warning   # accessibility modifiers required
dotnet_diagnostic.IDE0161.severity = warning   # file-scoped namespace
csharp_style_namespace_declarations = file_scoped:warning

# Code-quality analyzers
dotnet_diagnostic.CA1822.severity = warning    # member can be static
dotnet_diagnostic.CA2007.severity = none       # ConfigureAwait not needed in apps
dotnet_diagnostic.CA1062.severity = none
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <AnalysisLevel>latest-recommended</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <NoWarn>CS1591</NoWarn>
  </PropertyGroup>
</Project>
namespace Std
{
    class FrameParser
    {
        int count;

        public int Count() => count;

        public string Describe(byte[] data)
        {
            var n = data.Length;
            return $"{n} bytes";
        }
    }
}
namespace Std;

internal sealed class FrameParser
{
    private int _count;

    public int Increment() => ++_count;

    public static string Describe(byte[] data)
    {
        int n = data.Length;
        return $"{n} bytes";
    }
}

Expected Result

The build of the first version reports every rule violation (MSBuild prints the list twice: once as they occur and once in the summary); the fixed version builds cleanly:

TryReadVin: EHBAUREV250000001
TryReadVin: no answer (expected failure, no exception)
guard clause: ArgumentNullException (ecu)
ReadVinAsync honoured the cancellation token
old API still works: EHBAUREV250000001
ReadDtcs(IEnumerable<string>) -> IReadOnlyList<string>: VCU:P0A80,BMS:P0A80
FlashAsync(FlashOptions) -> FlashResult { Success = True, Message = VCU flashed with vcu.hex, CRC verified: True }
m60_obsolete.cs(2,19): warning CS0618: 'DiagnosticClient.GetVin(string)' is obsolete: 'Use ReadVinAsync. GetVin will be removed in version 3.0.'
> dotnet build -nologo -v q
C:\demo\Parser.cs(5,13): warning CS0649: Field 'FrameParser.count' is never assigned to, and will always have its default value 0 [C:\demo\Std.csproj]
C:\demo\Parser.cs(5,13): warning IDE0040: Accessibility modifiers required (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0040) [C:\demo\Std.csproj]
C:\demo\Parser.cs(3,11): warning IDE0040: Accessibility modifiers required (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0040) [C:\demo\Std.csproj]
C:\demo\Parser.cs(1,11): warning IDE0161: Convert to file-scoped namespace (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0161) [C:\demo\Std.csproj]
C:\demo\Parser.cs(11,13): warning IDE0008: Use explicit type instead of 'var' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0008) [C:\demo\Std.csproj]
C:\demo\Parser.cs(9,23): warning CA1822: Member 'Describe' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822) [C:\demo\Std.csproj]
C:\demo\Parser.cs(3,11): warning CA1852: Type 'FrameParser' can be sealed b
…

Common Mistakes

Exposing List<T> or mutable arrays from public APIs (callers can change your state).

Boolean parameters (Flash("VCU", true, false)) — unreadable; use options or enums.

Missing CancellationToken on async methods; async void public methods.

Throwing Exception or returning null for errors without documentation.

Removing or renaming public members in a patch release.

Style debates in code review instead of an .editorconfig.

Disabling analyzer warnings globally instead of fixing or justifying them locally.

Best Practices

Design the API from the caller's code first (write usage examples/tests).

Keep the public surface minimal (internal by default, sealed unless designed for inheritance).

Enable nullable reference types, analyzers and warnings-as-errors in CI.

Document public members, including exceptions and thread-safety.

Use package validation to detect breaking changes before publishing.

Interview Relevance

Medium–high: "how do you design a public API?", "how do you evolve an API without breaking clients?", "Try-pattern vs exceptions?", "where does CancellationToken go?", "how does your team enforce coding standards?", "what do you look for in code review?".

Practice Questions

Add ReadDtcsAsync(IEnumerable<string>, CancellationToken) returning IAsyncEnumerable<Dtc>.

Enable <EnablePackageValidation> on the library from Module 46, remove a public method, and observe the error.

Add a naming rule that requires interface names to start with I and trigger it.

Run dotnet format on the "bad" parser and compare the result.

Quick Quiz

Q1. (Multiple choice) Where should a CancellationToken parameter go? A) First B) Last, with a default C) In a property D) Nowhere; use a global token Answer: B.

Q2. (Predict the output) Calling a member marked [Obsolete("...")].

Answer: Warning CS0618 with the message (verified); [Obsolete("...", true)] makes it an error.

Q3. (True/False) EnforceCodeStyleInBuild makes IDE style rules appear in dotnet build output.

Answer: True (verified: IDE0040, IDE0161, IDE0008 appeared).

Q4. (Find the bug) public List<string> Dtcs { get; } = new(); on a public library class.

Answer: Callers can add, remove or clear items behind the class's back; expose IReadOnlyList<string> and keep the list private.

Coding Challenge

Design a TryParseCanId(ReadOnlySpan<char> text, out uint id) API that accepts "0x7E0" or "7E0", rejects identifiers above 0x1FFFFFFF, and never throws; test it with four inputs.

using System.Globalization;

foreach (string s in new[] { "0x7E0", "18FEF100", "0xZZ", "0x3FFFFFFF" })
    Console.WriteLine(TryParseCanId(s, out uint id) ? $"{s,-11} -> 0x{id:X}" : $"{s,-11} -> invalid");

static bool TryParseCanId(ReadOnlySpan<char> text, out uint id)
{
    if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) text = text[2..];
    if (uint.TryParse(text, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out id) && id <= 0x1FFFFFFF)
        return true;
    id = 0;                      // out parameters get a defined value on failure
    return false;
}