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?


1 High‑level principles

  1. SOLID first – especially Interface & Dependency Inversion.
  2. 100‑line rule – any file > 100 physical lines must be split or refactored.
  3. Contract‑level ownership – public abstractions live in lightweight Contracts libraries; impl classes live in runtime projects.
  4. Single Composition Root – all DI wiring happens in StellaOps.Web/Program.cs and in each plug‑in’s IoCConfigurator; nothing else calls IServiceCollection.BuildServiceProvider.
  5. No Service Locator – constructor injection only; static ServiceProvider is banned.
  6. Fail‑fast startup – configuration validated before the web‑host listens.
  7. 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

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

11 Commit & PR checklist

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.