Перейти к основному содержимому

Стандарты Качества Кода — Enterprise .NET

Назначение

Документ фиксирует обязательные стандарты качества кода для всех backend разработчиков Pin. Правила специфичны для fintech/gambling платформы: работа с деньгами, PII, регуляторными данными и лицензионными требованиями делает «почти правильно» неприемлемым.

Строгая Типизация

  • #nullable enable и <WarningsAsErrors>nullable</WarningsAsErrors> в каждом проекте.
  • null! запрещен в domain и application коде без явного комментария-обоснования.
  • object, dynamic, JsonDocument, Dictionary<string, object> запрещены в domain/application contracts.
  • Nullable коллекции запрещены — возвращать пустую коллекцию [], не null.
  • Enum вместо boolean flags и строковых статусов для всех состояний домена.
  • «Анонимные» строковые enum'ы запрещены. Значение из фиксированного набора (например, режим снапшота, флаги регулярного выражения, тип ссылки, роль и уровень доступа) моделируется реальным C# enum, а не string/string?. Правило распространяется на API-контракты (DTO), доменные value object и JSONB-модели. Набор флагов — IReadOnlyList<TEnum> (как FieldFlag), а не строка с разделителями. Каждый enum описывается в реестре docs/03-domain/Pin/02-domain-model/enums/: значения PascalCase, хранение в БД как text через .HasConversion<string>(), сериализация глобальным JsonStringEnumConverter(allowIntegerValues: false), описанная обработка unknown.
  • MoneyAmount(decimal Value, short CurrencyId) — деньги не хранятся как decimal без валюты; CurrencyId — id справочника handbook (ADR-0063).

Immutability

  • record запрещён. Для value objects, DTO, events, commands, queries, results, forms, JSONB-VO используется sealed class с свойствами { get; init; } (init — единственный сеттер) и квалификатором public required для обязательных полей. Позиционные record-конструкторы не применяются; объекты создаются через object-initializer (new X { Prop = value }). Причина: явная инициализация по именам, отсутствие неявного value-equality/with, единообразие с aggregate root.
  • required ставится только для non-nullable обязательных свойств. Для nullable-свойств (T?) required не используется (они опциональны по умолчанию).
  • Value-type записи (record struct) заменяются на readonly struct с { get; init; } (запрет распространяется и на record struct).
  • Исключение — доменные события: они наследуют общий базовый DomainEvent (тип record в src/shared/Talent.Common). Пока этот общий тип — record, наследники-события остаются record. Перевод иерархии событий на class выполняется отдельно, вместе с изменением shared-слоя.
  • Aggregate root — sealed class с private set на всех свойствах; изменения только через domain methods.
  • sealed по умолчанию для всех классов, где наследование не проектировалось явно.
  • init-only properties для данных, задаваемых один раз при создании.

Инварианты И Guard Clauses

  • Aggregate factory methods (Create(...)) всегда валидируют через Guard.*.
  • Value Object конструктор бросает DomainException при нарушении инварианта.
  • Application handler не использует Guard — использует FluentValidation и Result.Fail.
  • Illegal state transition → Result.Fail(new InvalidStatusTransitionError(...)), не throw.

Async / Await

  • CancellationToken принимается во всех методах с I/O.
  • .Result, .Wait(), .GetAwaiter().GetResult() запрещены в production async контексте.
  • ConfigureAwait(false) в library и infrastructure коде.
  • Собственные методы не используют суффикс Async (framework методы — используют).
  • IAsyncEnumerable<T> для потоковых операций (export, bulk read) вместо List<T> in-memory.
  • ValueTask для high-frequency методов с синхронным fast-path.

Именование И Константы

  • Magic strings/numbers запрещены — использовать const, enum или static readonly.
  • Cache keys — через static string CacheKeys.<Name>(...) методы, не inline строки.
  • Enum значения хранятся в PostgreSQL как string (.HasConversion<string>()).
  • Metric names — snake_case, формат <service>_<entity>_<operation>_<unit>.

Exceptions

  • Бизнес-ошибки — Result.Fail(new TypedError(...)), не throw.
  • Нарушение инварианта домена — throw new DomainException(message).
  • Unreachable code в switch — throw new UnreachableException(...).
  • catch (Exception e) { /* ignore */ } — запрещен. Либо логировать, либо пробрасывать.
  • InvalidOperationException — только для неверной конфигурации или startup проблем.

Pattern Matching

  • switch expressions вместо if/else для state machine mapping.
  • Exhaustive pattern matching с _ case, бросающим UnreachableException.
  • is-pattern для typed result branches и nullable checks.

Код Файла

  • Один public тип — один файл. Нет partial class без явной причины (source gen — исключение).
  • Файл не превышает 300 строк. Если длиннее — разделить на smaller concerns.
  • using declarations в начале файла, system namespaces первые.
  • #region запрещены — структурировать через методы и классы.

Code Review Блокеры

Pull request не принимается, если:

  • нет типов на nullable коллекциях или nullable reference типах;
  • dynamic или object используются в domain/application коде;
  • бизнес-ошибка выражена через throw, а не Result.Fail;
  • нет CancellationToken в I/O методах;
  • magic string/number без константы;
  • «анонимный» строковый enum (свободная string) вместо реального C# enum в контракте, домене или JSONB-модели;
  • sealed отсутствует на domain/application классе без причины.

Детальный Справочник

13 — Code Quality And Typing — полное руководство с примерами кода.

Обратные ссылки

Автоссылки: где используется этот документ.