8.1 KiB
18 · Coding Standards & Contributor Guide — Stella Ops
(v2.0 — 12 Jul 2025 · supersedes v1.0)
Audience — Anyone sending a pull‑request to the open‑source Core.
Goal — Keep the code‑base small‑filed, plug‑in‑friendly, DI‑consistent, and instantly readable.
0 Why read this?
- Cuts review time → quicker merges.
- Guarantees code is hot‑load‑safe for run‑time plug‑ins.
- Prevents style churn and merge conflicts.
1 High‑level principles
- SOLID first – especially Interface & Dependency Inversion.
- 100‑line rule – any file > 100 physical lines must be split or refactored.
- Contract‑level ownership – public abstractions live in lightweight Contracts libraries; impl classes live in runtime projects.
- Single Composition Root – all DI wiring happens in
StellaOps.Web/Program.cs
and in each plug‑in’sIoCConfigurator
; nothing else callsIServiceCollection.BuildServiceProvider
. - No Service Locator – constructor injection only; static
ServiceProvider
is banned. - Fail‑fast startup – configuration validated before the web‑host listens.
- Hot‑load compatible – no static singletons that survive plug‑in unload; avoid
Assembly.LoadFrom
outside the built‑in plug‑in loader.
2 Repository layout (flat, July‑2025)**
src/
├─ backend/
│ ├─ StellaOps.Web/ # ASP.NET host + composition root
│ ├─ StellaOps.Common/ # Serilog, Result<T>, helpers
│ ├─ StellaOps.Contracts/ # DTO + interface contracts (no impl)
│ ├─ StellaOps.Configuration/ # Options + validation
│ ├─ StellaOps.Localization/
│ ├─ StellaOps.PluginLoader/ # Cosign verify, hot‑load
│ ├─ StellaOps.Scanners.Trivy/ # First‑party scanner
│ ├─ StellaOps.TlsProviders.OpenSsl/
│ └─ … (additional runtime projects)
├─ plugins-sdk/ # Templated contracts & abstractions
└─ frontend/ # Angular workspace
tests/ # Mirrors src structure 1‑to‑1
There are no folders named “Module” and no nested solutions.
3 Naming & style conventions
Element | Rule | Example |
---|---|---|
Namespaces | File‑scoped, StellaOps. | namespace StellaOps.Scanners; |
Interfaces | I prefix, PascalCase | IScannerRunner |
Classes / records | PascalCase | ScanRequest, TrivyRunner |
Private fields | camelCase (no leading underscore) | redisCache, httpClient |
Constants | SCREAMING_SNAKE_CASE | const int MAX_RETRIES = 3; |
Async methods | End with Async | Task ScanAsync() |
File length | ≤ 100 lines incl. using & braces | enforced by dotnet format check |
Using directives | Outside namespace, sorted, no wildcards | — |
Static analyzers (.editorconfig, StyleCop.Analyzers package) enforce the above.
4 Dependency‑injection policy
Composition root – exactly one per process:
builder.Services
.AddStellaCore() // extension methods from each runtime project
.AddPluginLoader("/Plugins", cfg); // hot‑load signed DLLs
Plug‑ins register additional services via the IoCConfigurator convention described in the Plug‑in SDK Guide, §5. Never resolve services manually (provider.GetService()) outside the composition root; tests may use WebApplicationFactory or ServiceProvider.New() helpers. Scoped lifetime is default; singletons only for stateless, thread‑safe helpers.
5 Project organisation rules
Contracts vs. Runtime – public DTO & interfaces live in .Contracts; implementation lives in sibling project. Feature folders – inside each runtime project group classes by use‑case, e.g.
├─ Scan/
│ ├─ ScanService.cs
│ └─ ScanController.cs
├─ Feed/
└─ Tls/
Tests – mirror the structure under tests/ one‑to‑one; no test code inside production projects.
6 C# language features
Nullable reference types enabled. record for immutable DTOs. Pattern matching encouraged; avoid long switch‑cascades. Span & Memory OK when perf‑critical, but measure first. Use await foreach over manual paginator loops.
7 Error‑handling template
public async Task<IActionResult> PostScan([FromBody] ScanRequest req)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
try
{
ScanResult result = await scanService.ScanAsync(req);
if (result.Quota != null)
{
Response.Headers.TryAdd("X-Stella-Quota-Remaining", result.Quota.Remaining.ToString());
Response.Headers.TryAdd("X-Stella-Reset", result.Quota.ResetUtc.ToString("o"));
}
return Ok(result);
}
}
RFC 7807 ProblemDetails for all non‑200s. Capture structured logs with Serilog’s message‑template syntax.
8 Async & threading
- All I/O is async; no .Result / .Wait().
- Library code: ConfigureAwait(false).
- Limit concurrency via Channel or Parallel.ForEachAsync, never raw Task.Run loops.
9 Testing rules
Layer | Framework | Coverage gate |
---|---|---|
Unit | xUnit + FluentAssertions | ≥ 80 % line, ≥ 60 % branch |
Integration | Testcontainers | Real Redis & Trivy |
Mutation (critical libs) | Stryker.NET | ≥ 60 % score |
One test project per runtime/contract project; naming .Tests.
10 Static analysis & formatting
- dotnet format must exit clean (CI gate).
- StyleCop.Analyzers + Roslyn‑Security‑Guard run on every PR.
- CodeQL workflow runs nightly on main.
11 Commit & PR checklist
- Conventional Commit prefix (feat:, fix:, etc.).
- dotnet format & dotnet test both green.
- Added or updated XML‑doc comments for public APIs.
- File count & length comply with 100‑line rule.
- If new public contract → update relevant markdown doc & JSON‑Schema.
12 Common pitfalls
Symptom | Root cause | Fix |
---|---|---|
InvalidOperationException: Cannot consume scoped service... | Mis‑matched DI lifetimes | Use scoped everywhere unless truly stateless |
Hot‑reload plug‑in crash | Static singleton caching plugin types | Store nothing static; rely on DI scopes |
100‑line style violation |Large handlers or utils |Split into private helpers or new class
13 Change log
Version | Date | Notes |
---|---|---|
v2.0 | 2025‑07‑12 | Updated DI policy, 100‑line rule, new repo layout, camelCase fields, removed “Module” terminology. |
1.0 | 2025‑07‑09 | Original standards. |