Centralize SDK and threading analyzers, baseline the .NET 10 All rule inventory at suggestion severity, and promote S3981 repo-wide. Fix the always-true worker count predicate and cover the idle/active branches. Fixes #15 Co-Authored-By: Codex <codex@openai.com>
9.5 KiB
Contributing to the ErsatzTV fork
A descriptive guide to the conventions already in this codebase — written so future changes (human or AI) extend the established architecture instead of reinventing it. It's a living document: update it when a pattern legitimately evolves.
The guiding principle
Match the established style and patterns. Diverge only when there's a genuinely strong reason — when it's the real best or only option — even if that means rearchitecting. Never deviate gratuitously or just because a different approach is locally convenient. When a deviation is justified, state why (in the PR/commit, and in code comments where it'll surprise a reader).
Several of the rules below are now enforced in CI (layering tests, analyzers, formatting, migration checks) — see the pointers per section.
1. Architecture & layering
CQRS via MediatR. Dependency direction is enforced by ErsatzTV.Architecture.Tests (NetArchTest):
| Project | Responsibility | May depend on |
|---|---|---|
ErsatzTV.FFmpeg |
ffmpeg process/pipeline wrapper | (nothing — lowest layer) |
ErsatzTV.Core |
domain entities, interfaces, pure logic | FFmpeg only — no EF Core, no Infrastructure/Application |
ErsatzTV.Infrastructure |
EF Core data access, external clients | Core |
ErsatzTV.Infrastructure.Sqlite / .MySql |
provider-specific EF (migrations) | Core, Infrastructure |
ErsatzTV.Application |
MediatR handlers (business logic) | Core, Infrastructure (the abstraction, not the concrete providers) |
ErsatzTV.Scanner |
library scanning host | Core, Infrastructure* |
ErsatzTV |
ASP.NET Core host, controllers, DI composition root | everything |
If a test fails here, the fix is almost always to move the type, not to relax the rule. (ersatztv#12)
2. CQRS handler conventions
- Requests are
recordtypes named for the action —GetX/CreateX/UpdateX/DeleteX— implementingIRequest<TResult>, living under<Domain>/Queries/or<Domain>/Commands/next to their handler. - Result types (LanguageExt, see §3): queries return
Option<T>orList<T>; commands returnOption<BaseError>(void success) orEither<BaseError, T>(value success). - Handlers implement
IRequestHandler<TRequest, TResult>with primary-constructor DI and anasync Task<TResult> Handle(TRequest request, CancellationToken cancellationToken). - Validation lives in the handler as private static methods returning
Validation<BaseError, T>, composed with tuple.Apply(...)— not FluentValidation or MediatR pipeline behaviors. - Examples:
ErsatzTV.Application/Channels/Queries/GetChannelById.cs(+…/GetChannelByIdHandler.cs,Option<ChannelViewModel>),ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs(tuple-Applyvalidation →Either<BaseError, Unit>).
3. Functional style (LanguageExt)
Every project globally imports using static LanguageExt.Prelude;. Use the monads instead of
exceptions for control flow.
Option<T>—Optional(x),Some/None,HeadOrNone()to pluck from a collection; branch withMatch/MatchAsync(onSome, onNone); default withIfNone(...); transform withMap/Bind;foreach (var x in option)to unpack conditionally.Either<BaseError, T>/Validation<BaseError, T>— the error backbone.BaseError.New("…")for failures,Unit.Defaultfor void success. Accumulate multiple checks withValidation(tuple.Apply((a, b, c) => …)) thenToEither().MapLeftto transform errors.- Examples:
ErsatzTV.Core/FFmpeg/…,ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs.
4. Web UI (ChicoryTV SPA)
The legacy Blazor Server UI + MudBlazor were removed in #91 phase (b). The ChicoryTV React SPA in
web/ (Vite + TypeScript, built into ErsatzTV/wwwroot/app, served at /app) is the only UI; it
talks exclusively to the REST API. Every former Blazor route now 302-redirects to its /app equivalent
(ErsatzTV/LegacyUiRedirects.cs + the Startup catch-all).
- Adding a screen: see
docs/spa-conventions.mdfor the SPA screen playbook. - Endpoints the screen calls: see
docs/api-conventions.mdfor the endpoint checklist. - Keep the UI thin — screens call
/api/*; business logic stays in MediatR handlers.
5. Data access & EF Core
TvContext(ErsatzTV.Infrastructure/Data/TvContext.cs) is the single DbContext; resolve it viaIDbContextFactory<TvContext>in handlers.- Two providers:
ErsatzTV.Infrastructure.Sqlite(the prod default,/config/ersatztv.sqlite3) and…MySql, each with its ownMigrations/set +TvContextModelSnapshot. The provider is chosen by theproviderconfig key inStartup. - A model change needs a migration in BOTH providers — run
scripts/add-migration.sh <Name>. CI'smigrationsjob enforces model-drift + apply-to-fresh-DB per provider (seedocs/ci-cd.md→ Migration integrity). Review non-transactional ops (e.g. SQLitePRAGMA foreign_keys) carefully. (ersatztv#13)
6. FFmpeg pipeline
- Step-based composition: each task is an
IPipelineStep(ErsatzTV.FFmpeg/IPipelineStep.cs) that contributesGlobalOptions/InputOptions/FilterOptions/OutputOptionsand aNextState()that threads frame metadata (pixel format, data location, dimensions).CommandGenerator.GenerateArgumentsassembles the steps into the final argument list. - Detect capabilities at runtime; don't hardcode —
IHardwareCapabilitiesimplementations (Vaapi/Nvidia/…HardwareCapabilities) report available codecs/profiles, and builders pick the encoder/decoder accordingly. - Version-gate features explicitly — e.g.
Capabilities/NvidiaHardwareCapabilities.csparsesFFmpegCapabilities.Versionand disables 10-bit H.264 decode hwaccel below ffmpeg 8. Follow this pattern for the ffmpeg 8 upgrade (ersatztv#9). - Examples:
ErsatzTV.FFmpeg/Encoder/EncoderBase.cs,ErsatzTV.FFmpeg/Filter/ScaleFilter.cs.
7. Naming, formatting, analyzers
.editorconfigis the source of truth for formatting + rule severities (plusErsatzTV.sln.DotSettingsfor ReSharper). Rundotnet formatbefore committing.- Fix formatting as you touch it (no big-bang). ~2500 legacy
.csfiles inherited from upstream carry a UTF-8 BOM, which violates.editorconfig'scharset=utf-8. We do not mass-reformat. Instead, when you modify a file for other work, normalize it in that same PR —dotnet format ErsatzTV.sln --include <the files you touched>, which strips the BOM and fixes style. Files you did not touch stay as-is. This is enforced two ways so it can't be silently skipped (ersatztv#311): the pre-commit hook verifies staged.cs, and a blockingformatCI job re-verifies the.csthis PR changed against.editorconfig(a.cs-free PR passes trivially). Never--no-verifypast a format failure on a file you changed — de-BOM/format it instead. (A one-time repo-wide normalization is a separate, unmade decision; the touched-file rule is the standing one.) TreatWarningsAsErrors=truein the app projects — a warning fails the build.NoWarncarries a small, documented exemption list (e.g.VSTHRD200,CA1873); NuGet-auditNU1901-1903are demoted to warnings inDirectory.Build.propswhileNU1904(critical) blocks.- Static analysis is centralized:
Directory.Build.propsenables the SDK analyzers atlatest-Allplus the threading analyzer for every project;Directory.Build.targetsadds Roslynator, SonarAnalyzer, Meziantou, and AsyncFixer. The checked-in SDK globalconfig and.editorconfigkeep the broad baseline atsuggestion. Promote one reviewed rule at a time by setting it towarningand adding its ID to the centralWarningsAsErrorslist; never flip a wall of rules to error at once. Refresh the SDK globalconfig deliberately when moving to a new .NET SDK major. (ersatztv#15)
8. Testing
NUnit ([TestFixture]/[Test]/[TestCase]) + Shouldly + NSubstitute; xUnit is not
used. Golden-file tests guard the M3U/XMLTV output formats and a hard fail on a missing/changed
golden means broken code, not a stale fixture — never regenerate via ETV_UPDATE_GOLDENS=1 in CI
or from an agent. Full testing map (per-project coverage, golden-file details, timezone notes,
how to run subsets, the per-PR gate): docs/testing.md.
9. Build / CI
- Central Package Management: versions live in the repo-root
Directory.Packages.props; csproj reference packages by name only — never putVersion=back on a<PackageReference>(NU1008). Repo-wide MSBuild config is inDirectory.Build.props/Directory.Build.targets. (ersatztv#14) - The Gitea Actions pipeline (
.gitea/workflows/docker-build.yml):test→migrations→build(image + smoke/IPTV-E2E). Renovate opens dependency PRs; a weeklydependency-scansurfaces advisories. Full details:docs/ci-cd.md. - Versioning is CalVer
vYY.<release-seq>.<patch>(not year.month); never[skip ci]a commit you'll tag. Seedocs/ci-cd.md→ Versioning.
10. Deviation policy
Re-stating §0 because it's the whole point: follow the established pattern; diverge only with a concrete, stated reason. Record an intentional departure in the PR description and a code comment at the point of surprise (e.g. "using X instead of the usual Y because …"). If a pattern itself should change, change it deliberately and update this guide in the same PR.