C# Coding Standards
Source:
Documentation/source/CLAUDE.mdThese are the enforced conventions for all AccordionQ2 C# code.
Project Context
AccordionQ2 is a C# .NET embedded application running on Raspberry Pi (ARM32/Linux). It exposes hardware instruments and industrial protocols (SCPI, Modbus, PMBus, HCI, etc.) to remote clients over a network protocol server.
Null Handling
// Constructor parameters — throw immediately on null
public MyClass(IFoo foo)
{
_foo = foo ?? throw new ArgumentNullException(nameof(foo));
}
// Optional/nullable members — safe invocation
proxy?.Stop();
// Null-coalescing before instance methods — never call directly on nullable string
var trimmed = (value ?? "").Trim();
// Prefer is null / is not null over == null
if (value is null) ...
if (result is not null) ...
Exception Handling
try
{
await DoWorkAsync(ct);
}
catch (OperationCanceledException) // ALWAYS catch separately, ALWAYS rethrow
{
throw;
}
catch (Exception ex)
{
Logger.Error(log4net, ex); // log via IFileLogging wrapper
throw; // use throw; not throw ex;
}
// Cleanup / dispose — use bare catch only here
try { resource.Dispose(); } catch { /* best effort */ }
Logging
Every class declares a static logger:
private static readonly log4net.ILog log4net =
LogHelper.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
Always call through the IFileLogging wrapper — never call log4net.Info(...) directly:
Logger.Info(log4net, "Module loaded");
Logger.Debug(log4net, $"Processing item {id}");
Logger.Warn(log4net, "Config value missing, using default");
Logger.Error(log4net, ex);
| Level | Use for |
|---|---|
Info |
Operational events (start, stop, load, connect) |
Debug |
Per-operation detail, verbose tracing |
Warn |
Configuration issues, recoverable problems |
Error |
All caught exceptions |
Async
// All async methods return Task or Task<T>
public async Task<string> GetValueAsync(CancellationToken ct = default) { ... }
// Never async void
// ✗ public async void OnEvent() { ... }
// ✓ public async Task OnEventAsync() { ... }
// Always accept and thread through CancellationToken
public async Task ProcessAsync(CancellationToken ct)
{
await _client.DoAsync(ct).ConfigureAwait(false);
}
// Suffix all async methods with Async
public Task StartAsync(CancellationToken ct) { ... }
// Fire-and-forget: discard explicitly
_ = Task.Run(() => BackgroundWork());
Naming
| Element | Convention | Example |
|---|---|---|
| Classes, methods, properties, events | PascalCase | ModuleManager, GetValueAsync |
| Private fields | _camelCase |
_httpClient, _channelMap |
| Local variables | camelCase |
moduleCount, statusMessage |
| Boolean methods | Is* / Has* prefix |
IsConnected(), HasErrors() |
| Abbreviations | Preserve casing | HAL, Uart, SCPI, mDNS |
General Style
// var for non-obvious types; explicit for primitives and when clarity matters
var modules = await client.GetAllAsync();
int count = modules.Count;
// String interpolation preferred
var msg = $"Loaded {count} modules in {elapsed.TotalMs:F1} ms";
// LINQ preferred over loops when readable
var names = modules.Where(m => m.Enabled).Select(m => m.Name).ToList();
// private readonly for all non-mutating fields
private readonly HttpClient _http;
private readonly string _baseUrl;
// One class per file
// #region to group logical sections in large classes
Patterns to Avoid
| Anti-pattern | Why |
|---|---|
| Mutable statics | Thread safety; loggers are the only static readonly exception |
record types |
Not used in this codebase |
| Global usings | Explicit usings only |
| Reflection (except logger init and module loading) | Complexity, performance |
async void |
Unobservable exceptions |
throw ex; |
Destroys stack trace — always use throw; |
| Calling instance methods on potentially-null strings | Use (s ?? "").Trim() pattern |