13Code Quality и Static Analysis

Уровень 1: Foundation

Roslyn Analyzers

AST и Syntax Tree

Roslyn — компилятор-платформа .NET, предоставляющая API для анализа кода. Код представляется в виде Abstract Syntax Tree (AST), где каждый элемент — SyntaxNode (контейнер) или SyntaxToken (лист).

public int Add(int a, int b) { return a + b; }
MethodDeclaration (Add)
        ├── ReturnStatement
        │   ├── BinaryExpression (+)
        │   │   ├── IdentifierName (a)
        │   │   └── IdentifierName (b)
        ├── ParameterList
        │   ├── Parameter (int a)
        │   └── Parameter (int b)
        └── braces

Ключевые классы

КлассНазначение
SyntaxTreeДерево синтаксиса файла (из CSharpSyntaxTree.ParseText)
SyntaxNodeУзел дерева (класс, метод, выражение, оператор)
SyntaxTokenЛист дерева (ключевые слова, идентификаторы, операторы)
SyntaxTokenList / SyntaxNodeListКоллекции токенов/узлов
SyntaxVisitor / SyntaxWalkerПаттерн Visitor для обхода дерева

SyntaxWalker vs SyntaxVisitor

// SyntaxWalker — простой обход, можно контролировать порядок
        public class MyWalker : CSharpSyntaxWalker
        {
            public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
            {
                base.VisitMethodDeclaration(node);
            }
        }

        // SyntaxVisitor — полный контроль, каждый тип узла имеет свой метод
        public class MyVisitor : CSharpSyntaxVisitor
        {
            public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
            {
                node.ReturnStatement?.Accept(this);
            }
        }

DiagnosticAnalyzer

DiagnosticAnalyzer — базовый класс для создания правил анализа. Определяет диагностические правила с ID, заголовком, описанием и уровнем серьёзности.

Структура анализатора

[DiagnosticAnalyzer(LanguageNames.CSharp)]
        public class SynchronousHttpAnalyzer : DiagnosticAnalyzer
        {
            public const string DiagnosticId = "MY0001";
            private static readonly string Category = "Usage";

            private static readonly DiagnosticDescriptor Rule = new(
                DiagnosticId,
                title: "Synchronous HTTP call detected",
                messageFormat: "Avoid synchronous HTTP calls: {0}. Use async alternative.",
                Category,
                DiagnosticSeverity.Warning,
                isEnabledByDefault: true,
                description: "Synchronous HTTP calls can block threads and degrade performance.");

            public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => 
                ImmutableArray.Create(Rule);

            public override void Initialize(AnalysisContext context)
            {
                context.EnableConcurrentExecution();
                context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

                // Syntax-based: регистрируем действие при встрече синтаксиса
                context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);

                // Operation-based: регистрируем действие при встрече операции
                context.RegisterOperationAction(AnalyzeInvocationOperation, OperationKind.Invocation);
            }
        }

Syntax-based vs Operation-based анализ

АспектSyntax-basedOperation-based
ДоступТолько к ASTК AST + семантической модели
ТипыНет (нужно запрашивать)IMethodSymbol, ITypeSymbol
ПроизводительностьБыстрее (нет semantic model)Медленнее (нужен semantic model)
ТочностьМожет ложные срабатыванияУчитывает перегрузки, приведения
GeneratedCodeРаботаетМожно исключить через ConfigureGeneratedCodeAnalysis

Рекомендация: Используйте Operation-based анализ для правил, зависящих от типов и методов. Используйте Syntax-based для простых проверок структуры кода.


CodeFixProvider

CodeFixProvider предоставляет автоматические исправления для диагностик.

Реализация

[ExportCodeFixProvider(LanguageNames.CSharp, Shared = true)]
        public class SynchronousHttpCodeFix : CodeFixProvider
        {
            public override ImmutableArray<string> FixableDiagnosticIds => 
                ImmutableArray.Create("HTTP0001");

            public override FixAllProvider GetFixAllProvider() => 
                WellKnownFixAllProviders.BatchFixer;

            public override async Task RegisterCodeFixesAsync(CodeFixContext context)
            {
                var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
                var diagnostic = context.Diagnostics.First();
                var span = diagnostic.Location.SourceSpan;

                var invocation = root!.FindToken(span.Start).Parent
                    ?.AncestorsAndSelf()
                    .OfType<InvocationExpressionSyntax>()
                    .First();

                if (invocation == null) return;

                context.RegisterCodeFix(
                    CodeAction.Create(
                        title: "Convert to async alternative",
                        createChangedDocument: c => ConvertToAsyncAsync(context.Document, invocation, c),
                        equivalenceKey: "ConvertToAsync"),
                    diagnostic);
            }
        }

BatchFixProvider

WellKnownFixAllProviders.BatchFixer позволяет применять исправление ко всем диагностикам в документе/проекте одной операцией.


Built-in Analyzers

Категории правил

ПрефиксКатегорияПример
CA**Microsoft Code AnalysisCA1001: Dispose patterns
CS**Compiler warnings/errorsCS8600: Nullability
IDE0Code StyleIDE0007: var keyword
IDE1Roslyn AnalyzersIDE0005: Unused using
RSRoslyn SDKRS1030: Analyzer constraints
SA**StyleCop AnalyzersSA1101: Prefix local calls
VSTHRDMicrosoft.ThreadingVSTHRD101: Avoid unsupported lambda

Ruleset файлы

Ruleset — XML файл, определяющий активные правила и их серьёзность:

<?xml version="1.0" encoding="utf-8"?>
        <RuleSet Name="TeamRules" ToolsVersion="17.0">
          <Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.CodeAnalysis.NetAnalyzers">
            <Rule Id="CA1001" Action="Warning" />
            <Rule Id="CA1062" Action="Warning" />
            <Rule Id="CA2007" Action="Warning" />
          </Rules>

          <Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp" RuleNamespace="Microsoft.CodeAnalysis.CSharp">
            <Rule Id="IDE0011" Action="Warning" />
            <Rule Id="IDE0005" Action="Info" />
          </Rules>

          <!-- Отключаем конкретное правило -->
          <Rules AnalyzerId="StyleCop.CSharp.ReadabilityRules" RuleNamespace="StyleCop.CSharp.ReadabilityRules">
            <Rule Id="SA1137" Action="None" />
          </Rules>
        </RuleSet>

Применение в .csproj:

<PropertyGroup>
          <AnalysisRuleSet>$(MSBuildThisFileDirectory)Rules.ruleset</AnalysisRuleSet>
          <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
        </PropertyGroup>

Практика

Шаг 1: Создание проекта

dotnet new analyzer -o SynchronousHttpAnalyzer
        cd SynchronousHttpAnalyzer

Шаблон создаёт два проекта:

  • SynchronousHttpAnalyzer — сам анализатор
  • SynchronousHttpAnalyzer.Tests — юнит-тесты

Шаг 2: Реализация анализатора

using System.Collections.Immutable;
        using Microsoft.CodeAnalysis;
        using Microsoft.CodeAnalysis.Diagnostics;

        namespace SynchronousHttpAnalyzer;

        [DiagnosticAnalyzer(LanguageNames.CSharp)]
        public class SynchronousHttpAnalyzer : DiagnosticAnalyzer
        {
            public const string DiagnosticId = "HTTP0001";
            public const string Category = "Performance";

            private static readonly DiagnosticDescriptor Rule = new(
                DiagnosticId,
                title: "Synchronous HTTP call",
                messageFormat: "Use '{0}' instead of '{1}' to avoid thread blocking",
                category: Category,
                defaultSeverity: DiagnosticSeverity.Warning,
                isEnabledByDefault: true,
                description: "Synchronous HTTP calls block threads and reduce scalability.");

            public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics 
                => ImmutableArray.Create(Rule);

            public override void Initialize(AnalysisContext context)
            {
                context.EnableConcurrentExecution();
                context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
                context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
            }

            private static void AnalyzeInvocation(OperationAnalysisContext context)
            {
                var invocation = (IInvocationOperation)context.Operation;
                var method = invocation.TargetMethod;

                if (!method.ContainingType?.ToString()
                        .StartsWith("System.Net.Http.HttpClient") ?? true)
                    return;

                var name = method.Name;
                if (name == "Send" && !name.EndsWith("Async"))
                {
                    context.ReportDiagnostic(Diagnostic.Create(Rule,
                        context.Operation.Syntax.GetLocation(),
                        name + "Async", name));
                }
            }
        }

Шаг 3: Реализация CodeFix

using Microsoft.CodeAnalysis.CodeActions;
        using Microsoft.CodeAnalysis.CodeFixes;
        using Microsoft.CodeAnalysis.CSharp.Syntax;
        using Microsoft.CodeAnalysis;
        using System.Collections.Immutable;
        using System.Threading;
        using System.Threading.Tasks;

        namespace SynchronousHttpAnalyzer;

        [ExportCodeFixProvider(LanguageNames.CSharp, Shared = true)]
        public class SynchronousHttpCodeFix : CodeFixProvider
        {
            public override ImmutableArray<string> FixableDiagnosticIds 
                => ImmutableArray.Create("HTTP0001");

            public override FixAllProvider GetFixAllProvider() 
                => WellKnownFixAllProviders.BatchFixer;

            public override async Task RegisterCodeFixesAsync(CodeFixContext context)
            {
                var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
                var diagnostic = context.Diagnostics.First();
                var span = diagnostic.Location.SourceSpan;

                var invocation = root!.FindToken(span.Start).Parent
                    ?.AncestorsAndSelf()
                    .OfType<InvocationExpressionSyntax>()
                    .FirstOrDefault();

                if (invocation == null) return;

                context.RegisterCodeFix(
                    CodeAction.Create(
                        "Convert to async",
                        c => ReplaceWithAsync(context.Document, invocation, c),
                        equivalenceKey: "ConvertToAsync"),
                    diagnostic);
            }

            private static async Task<Document> ReplaceWithAsync(
                Document document, 
                InvocationExpressionSyntax invocation, 
                CancellationToken ct)
            {
                var root = await document.GetSyntaxRootAsync(ct);

                if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
                {
                    var newName = memberAccess.Name.Identifier.Text + "Async";
                    var newMemberAccess = memberAccess.WithName(
                        memberAccess.Name.With(SyntaxFactory.Identifier(newName)));
                    var newInvocation = invocation.WithExpression(newMemberAccess);

                    var newRoot = root!.ReplaceNode(invocation, newInvocation);
                    return document.WithSyntaxRoot(newRoot);
                }

                return document;
            }
        }

Шаг 4: Тестирование

using Microsoft.CodeAnalysis.CSharp.Testing.XUnit;
        using Xunit;

        public class SynchronousHttpAnalyzerTests
        {
            [Fact]
            public async Task HttpClient_Send_ShouldReportDiagnostic()
            {
                var testCode = """
                    using System.Net.Http;
                    var client = new HttpClient();
                    var response = client.Send(request);
                    """;

                var expected = VerifyC.Diagnostic("HTTP0001")
                    .WithLocation(4, 34)
                    .WithArguments("SendAsync", "Send");

                await VerifyC.VerifyAnalyzerAsync(testCode, expected);
            }

            [Fact]
            public async Task HttpClient_SendAsync_ShouldNotReportDiagnostic()
            {
                var testCode = """
                    using System.Net.Http;
                    var client = new HttpClient();
                    var response = await client.SendAsync(request);
                    """;

                await VerifyC.VerifyAnalyzerAsync(testCode);
            }
        }

Best Practices

Производительность

ПрактикаОписание
EnableConcurrentExecution()Всегда вызывайте в Initialize()
ConfigureGeneratedCodeAnalysisУстанавливайте None для кастомных анализаторов
OperationAction vs SyntaxActionOperation-based медленнее, но точнее
SupportedDiagnosticsКэшируйте ImmutableArray как static readonly

Дизайн правил

ПрактикаОписание
Уникальный IDДиапазон 0000-0999 для кастомных правил
SeverityWarning для рекомендаций, Error для блокирующих проблем
DescriptionПодробное описание с объяснением "почему" и "как исправить"
HelpLinkUriСсылка на документацию правила

Тестирование анализаторов

Используйте Microsoft.CodeAnalysis.Testing фреймворк:

// VerifyAnalyzerAsync — проверка что диагностика срабатывает
        await VerifyC.VerifyAnalyzerAsync(code, expectedDiagnostic);

        // VerifyCodeFixAsync — проверка что CodeFix работает
        await VerifyC.VerifyCodeFixAsync(code, expectedDiagnostic, fixedCode);

        // VerifyFixAsync — полная проверка: диагностика + исправление
        await VerifyC.VerifyFixAsync(analyzer, codeFix, expectedDiagnostic, fixedCode);

Ссылки

  • [Microsoft Learn: Write your first analyzer and code fix](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/tutorials/how-to-write-csharp-analyzer-code-fix)
  • [dotnet/roslyn-analyzers на GitHub](https://github.com/dotnet/roslyn-analyzers)
  • [AsyncFixer — популярный кастомный анализатор](https://github.com/semihokur/AsyncFixer)
  • [SonarSource sonar-dotnet (Roslyn-based)](https://github.com/sonarsource/sonar-dotnet)

Code Style и Conventions

.editorconfig — Centralized Configuration

.editorconfig — стандарт де-факто для определения правил форматирования кода. Поддерживается всеми основными IDE и редакторами.

Базовая структура

# Флаг: этот .editorconfig — корневой, не наследуем из вышестоящих директорий
        root = true

        # Глобальные настройки для всех файлов
        [*]
        indent_style = space
        indent_size = 4
        end_of_line = crlf
        charset = utf-8
        trim_trailing_whitespace = true
        insert_final_newline = true

        # Максимальная длина строки для подсветки
        max_line_length = 140
        guidelines = 120

        # Файлы проектов
        [*.{csproj,vbproj,props,targets}]
        indent_size = 2

        # JSON файлы
        [*.json]
        indent_size = 2

        # XML конфигурации
        [*.{config,nuspec,resx,xml}]
        indent_size = 2

        # Markdown
        [*.{md,rst}]
        trim_trailing_whitespace = false

        # Скрипты
        [*.{ps1,sh,bat}]
        indent_size = 2

.NET Code Style настройки

[*.{cs,vb}]

        #### .NET Coding Conventions ####

        # this. и Me. квалификаторы
        dotnet_style_qualification_for_field = false:suggestion
        dotnet_style_qualification_for_property = false:suggestion
        dotnet_style_qualification_for_method = false:suggestion
        dotnet_style_qualification_for_event = false:suggestion

        # Язык: ключевые слова вместо BCL типов
        dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
        dotnet_style_predefined_type_for_member_access = true:suggestion

        # Модификаторы
        dotnet_style_require_accessibility_modifiers = always:suggestion
        dotnet_style_readonly_field = true:suggestion

        # Expression-level preferences
        dotnet_style_object_initializer = true:suggestion
        dotnet_style_collection_initializer = true:suggestion
        dotnet_style_explicit_tuple_names = true:suggestion
        dotnet_style_null_propagation = true:suggestion
        dotnet_style_coalesce_expression = true:suggestion
        dotnet_style_operator_placement_when_wrapping = beginning_of_line

        # Auto properties
        dotnet_style_prefer_auto_properties = true:silent
        dotnet_style_prefer_compound_assignment = true:suggestion
        dotnet_style_prefer_conditional_expression_over_assignment = true:silent
        dotnet_style_prefer_conditional_expression_over_return = true:silent

        # Parentheses
        dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
        dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
        dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
        dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent

        #### Diagnostic configuration ####

        # Severity levels: silent, suggestion, warning, error
        dotnet_diagnostic.CA1001.severity = warning
        dotnet_diagnostic.CA1062.severity = warning
        dotnet_diagnostic.CA2007.severity = warning
        dotnet_diagnostic.SA1101.severity = warning
        dotnet_diagnostic.IDE0010.severity = suggestion
        dotnet_diagnostic.IDE0072.severity = suggestion

        #### C# Code Style ####

        # Modifier preferences
        csharp_preferred_modifier_order = public,private,protected,internal,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion

        # Implicit and explicit types
        csharp_style_var_for_built_in_types = true:suggestion
        csharp_style_var_when_type_is_apparent = true:suggestion
        csharp_style_var_elsewhere = true:suggestion

        # Expression-bodied members
        csharp_style_expression_bodied_methods = true:suggestion
        csharp_style_expression_bodied_constructors = true:suggestion
        csharp_style_expression_bodied_operators = true:suggestion
        csharp_style_expression_bodied_properties = true:suggestion
        csharp_style_expression_bodied_indexers = true:suggestion
        csharp_style_expression_bodied_accessors = true:suggestion
        csharp_style_expression_bodied_lambdas = true:suggestion
        csharp_style_expression_bodied_local_functions = true:suggestion

        # Pattern matching
        csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
        csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
        csharp_style_inlined_variable_declaration = true:suggestion
        csharp_style_throw_expression = true:suggestion
        csharp_style_null_propagation = true:suggestion

        # Namespace declarations
        csharp_style_namespace_declarations = file_scoped:suggestion

        #### C# Formatting Rules ####

        # New line preferences
        csharp_new_line_before_open_brace = all
        csharp_new_line_before_else = true
        csharp_new_line_before_catch = true
        csharp_new_line_before_finally = true
        csharp_new_line_before_members_in_object_initializers = true
        csharp_new_line_before_members_in_anonymous_types = true
        csharp_new_line_between_query_expression_clauses = true

        # Indentation
        csharp_indent_block_contents = true
        csharp_indent_braces = false
        csharp_indent_case_contents = true
        csharp_indent_case_contents_when_block = true
        csharp_indent_switch_labels = true
        csharp_indent_labels = no_change

        # Spacing
        csharp_space_after_cast = false
        csharp_space_after_colon_in_inheritance_clause = true
        csharp_space_after_comma = true
        csharp_space_after_dot = false
        csharp_space_after_semicolon_in_for_statement = true
        csharp_space_before_colon_in_inheritance_clause = true
        csharp_space_before_comma = false
        csharp_space_before_dot = false
        csharp_space_before_semicolon_in_for_statement = false
        csharp_space_between_empty_square_brackets = false
        csharp_space_between_parentheses = false

        # Wrapping
        csharp_preserve_single_line_statements = true
        csharp_preserve_single_line_blocks = true

        #### Organize Usings ####

        dotnet_sort_system_directives_first = true
        dotnet_separate_import_directive_groups = false

        #### Naming Conventions ####

        # Naming styles
        dotnet_naming_style.camel_case_style.capitalization = camel_case
        dotnet_naming_style.pascal_case_style.capitalization = pascal_case

        # Naming rules
        dotnet_naming_rule.private_instance_fields_with_underscore.severity = suggestion
        dotnet_naming_rule.private_instance_fields_with_underscore.symbols = private_instance_fields
        dotnet_naming_rule.private_instance_fields_with_underscore.style = camel_case_style
        dotnet_naming_rule.private_instance_fields_with_underscore.predicate = accessibility = private

        dotnet_naming_rule.private_static_fields.severity = suggestion
        dotnet_naming_rule.private_static_fields.symbols = private_static_fields
        dotnet_naming_rule.private_static_fields.style = pascal_case_style
        dotnet_naming_rule.private_static_fields.predicate = accessibility = private AND static = true

        dotnet_naming_rule.private_fields.severity = error
        dotnet_naming_rule.private_fields.symbols = all_private_fields
        dotnet_naming_rule.private_fields.style = underscore_style
        dotnet_naming_rule.private_fields.predicate = accessibility = private

        dotnet_naming_style.underscore_style.capitalization = camel_case
        dotnet_naming_style.underscore_style.prefix_0 = _

        dotnet_naming_symbols.all_private_fields.applicable_accessibilities = private
        dotnet_naming_symbols.private_instance_fields.applicable_accessibilities = private
        dotnet_naming_symbols.private_instance_fields.required_modifiers = 
        dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private
        dotnet_naming_symbols.private_static_fields.required_modifiers = static

        dotnet_naming_style.pascal_case_style.capitalization = pascal_case
        dotnet_naming_symbols.public_fields.applicable_accessibilities = public
        dotnet_naming_symbols.public_fields.required_modifiers = 
        dotnet_naming_rule.public_fields.severity = suggestion
        dotnet_naming_rule.public_fields.symbols = public_fields
        dotnet_naming_rule.public_fields.style = pascal_case_style

Global AnalyzerConfig

Global AnalyzerConfig — проектно-ориентированный файл настроек для Roslyn анализаторов.

is_global = true

        # Применение только к конкретным правилам
        dotnet_diagnostic.CA1001.severity = warning
        dotnet_diagnostic.CA2007.severity = error

        # Настройка параметров правила
        dotnet_diagnostic.CA1062.param._.severity = warning
        dotnet_diagnostic.CA1062.cfg[_].MinimumValidationLevel = 1

Применение в .csproj:

<ItemGroup>
          <GlobalAnalyzerConfigFiles Include="GlobalAnalyzerConfig.globalconfig" />
        </ItemGroup>

SonarQube Quality Profile

Архитектура

SonarQube Quality Profile
        ├── Bug Risk Rules (CAxxxx)
        ├── Security Vulnerability Rules (Sxxxx)
        ├── Code Smell Rules (Sxxxx)
        ├── Maintainability Rules (CAxxxx, IDE0xxx)
        └── Coverage & Analysis Thresholds

Настройка Quality Profile

# Создание кастомного профиля
        curl -u ADMIN_TOKEN -X POST \
          "http://localhost:9000/api/qualityprofiles/create?language=cs&profileName=EnterpriseProfile&sonarQube=false"

        # Активация правил
        curl -u ADMIN_TOKEN -X POST \
          "http://localhost:9000/api/qualityprofiles/activate_rules?activation=true&severity=MAJOR&key=S1066&parentKey=EnterpriseProfile"

        # Настройка threshold для severity
        curl -u ADMIN_TOKEN -X POST \
          "http://localhost:9000/api/qualityprofiles/change_language"

Quality Gate

{
          "name": "Enterprise Quality Gate",
          "conditions": [
            {
              "metric": "new_coverage",
              "op": "GT",
              "threshold": 80
            },
            {
              "metric": "new_reliability_rating",
              "op": "LE",
              "threshold": 1
            },
            {
              "metric": "new_security_rating",
              "op": "LE",
              "threshold": 1
            },
            {
              "metric": "new_maintainability_rating",
              "op": "LE",
              "threshold": 1
            },
            {
              "metric": "new_alert_status",
              "op": "NE",
              "threshold": "ERROR"
            },
            {
              "metric": "new_duplicated_lines_density",
              "op": "LE",
              "threshold": 3
            }
          ]
        }

Team Conventions vs Tool Defaults

Когда переопределять

СитуацияРекомендация
Команда не согласна с правиломПереопределить в .editorconfig с документированием причины
Legacy код не соответствуетПостепенное исправление, не отключать правило полностью
Бизнес-требованияПриоритет бизнес-требований над tool defaults
ПроизводительностьОтключить правило с обоснованием
Чужой код / third-partyНе переопределять, использовать exclude

Pre-commit hook для code style

# .pre-commit-config.yaml
        repos:
          - repo: local
            hooks:
              - id: dotnet-format
                name: dotnet format
                entry: dotnet format --verify-no-changes
                language: system
                types: [csharp]
                files: \.cs$
                pass_filenames: false

              - id: dotnet-build
                name: dotnet build
                entry: dotnet build --no-incremental
                language: system
                types: [csharp]
                files: \.csproj$
                pass_filenames: false

Checklist для Enterprise .editorconfig

  • [ ] root = true в корневом файле
  • [ ] Глобальные настройки ([*]) для всех файлов
  • [ ] Настройки для .cs / .vb файлов
  • [ ] Настройки для .csproj, .xml, .json
  • [ ] .NET Code Style правила (qualifiers, var, expression-bodied)
  • [ ] .NET Formatting правила (newlines, indentation, spacing)
  • [ ] Naming Conventions (camelCase, PascalCase, underscore для private)
  • [ ] Organize Usings (system directives first, group sort)
  • [ ] Diagnostic severity (CA, IDE, SA правила)
  • [ ] .editorconfig в корне репозитория
  • [ ] .editorconfig в CI pipeline (через EnforceCodeStyleInBuild)
  • [ ] Документированы все кастомные переопределения

Ссылки

  • [EditorConfig documentation](https://editorconfig.org/)
  • [.NET EditorConfig settings reference](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files)
  • [RehanSaeed/EditorConfig — comprehensive example](https://github.com/RehanSaeed/EditorConfig)
  • [dotnet/efcore .editorconfig](https://github.com/dotnet/efcore/blob/main/.editorconfig)
  • [SonarSource sonar-dotnet](https://github.com/sonarsource/sonar-dotnet)

Практика


Documentation Practices

XML Documentation Comments

XML documentation комментарии — стандартный способ документирования public API в C#. Генерируются в .xml файл и используются IntelliSense, Sandcastle, Doxygen и другими инструментами.

Базовые теги

/// <summary>
        /// Calculates the total order amount including tax and shipping.
        /// </summary>
        /// <param name="subtotal">The order subtotal before tax and shipping.</param>
        /// <param name="taxRate">The tax rate as a decimal (e.g., 0.08 for 8%).</param>
        /// <param name="shippingMethod">The selected shipping method.</param>
        /// <returns>The calculated total order amount.</returns>
        /// <exception cref="T:System.ArgumentException">Thrown when subtotal is negative.</exception>
        /// <exception cref="T:System.InvalidOperationException">
        /// Thrown when shipping method is not available for the destination.
        /// </exception>
        /// <remarks>
        /// This method applies tax before shipping costs. Tax is calculated based on
        /// the customer's region stored in the <paramref name="shippingMethod"/>.
        /// </remarks>
        /// <example>
        /// <code>
        /// var order = new Order { Subtotal = 100.00m };
        /// var total = OrderCalculator.CalculateTotal(100.00m, 0.08m, ShippingMethod.Standard);
        /// // total = 100.00 + 8.00 (tax) + 5.99 (shipping) = 113.99
        /// </code>
        /// </example>
        public decimal CalculateTotal(decimal subtotal, decimal taxRate, ShippingMethod shippingMethod)
        {
            if (subtotal < 0)
                throw new ArgumentException("Subtotal cannot be negative", nameof(subtotal));

            var tax = subtotal * taxRate;
            var shipping = shippingMethod switch
            {
                ShippingMethod.Standard => 5.99m,
                ShippingMethod.Express => 12.99m,
                ShippingMethod.Overnight => 24.99m,
                _ => throw new InvalidOperationException($"Unknown shipping method: {shippingMethod}")
            };

            return subtotal + tax + shipping;
        }

Полная таблица тегов

ТегНазначениеПример
<summary>Краткое описание элементаОбязательный для public API
<param name="...">Описание параметра<param name="cancellationToken">...</param>
<returns>Описание возвращаемого значенияДля методов с return
<exception cref="...">Исключения, которые может выбросить<exception cref="T:System.ArgumentNullException">...</exception>
<remarks>Дополнительные заметкиПодробное описание поведения
<example>Пример использования<example><code>...</code></example>
<seealso cref="...">Ссылка на связанный элемент<seealso cref="T:OrderService"/>
<value>Описание property<value>The collection of items.</value>
<typeparam name="...">Описание generic параметра<typeparam name="T">The element type.</typeparam>
<list>Список элементов<list type="bullet"><item>...</item></list>

XML Docs для Generic типов и методов

/// <summary>
        /// Represents a repository for storing and retrieving entities of type <typeparamref name="TEntity"/>.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity. Must have a parameterless constructor.</typeparam>
        /// <typeparam name="TKey">The type of the entity's primary key.</typeparam>
        public interface IRepository<TEntity, TKey> where TEntity : class
        {
            /// <summary>
            /// Retrieves an entity by its primary key.
            /// </summary>
            /// <param name="key">The primary key value.</param>
            /// <returns>The entity, or <c>null</c> if not found.</returns>
            TEntity? GetById(TKey key);

            /// <summary>
            /// Adds a new entity to the repository.
            /// </summary>
            /// <param name="entity">The entity to add.</param>
            /// <exception cref="T:System.ArgumentNullException">Thrown when <paramref name="entity"/> is null.</exception>
            void Add(TEntity entity);

            /// <summary>
            /// Finds entities matching the specified predicate.
            /// </summary>
            /// <param name="predicate">The filter predicate.</param>
            /// <returns>Collection of matching entities.</returns>
            IEnumerable<TEntity> Find(Expression&lt;Func&lt;TEntity, bool&gt;&gt; predicate);
        }

Generierung XML Doc файла

<!-- В .csproj -->
        <PropertyGroup>
          <GenerateDocumentationFile>true</GenerateDocumentationFile>
          <NoWarn>$(NoWarn);CS1591</NoWarn> <!-- CS1591: Missing XML comment for publicly visible type/member -->
        </PropertyGroup>

Markdown Documentation

README структура

# Project Name

        Short description of the project's purpose.

        ## Quick Start
        

dotnet restore dotnet build dotnet test dotnet run --project src/MyApp


        ## Architecture

        See [Architecture Overview](docs/architecture/overview.md) for detailed design.

        ## API Reference

        - [API Documentation](docs/api/README.md)
        - [OpenAPI Spec](openapi.yaml)

        ## Contributing

        1. Fork the repository
        2. Create a feature branch: `git checkout -b feature/my-feature`
        3. Run tests: `dotnet test`
        4. Submit a pull request

        ## License

        MIT License - see LICENSE file for details.

Architecture Decision Records (ADRs)

# docs/architecture/ADRs/README.md

        # Architecture Decision Records

        | ID | Title | Status | Date |
        |----|-------|--------|------|
        | ADR-001 | Use PostgreSQL as primary database | Accepted | 2024-01-15 |
        | ADR-002 | Adopt MassTransit for messaging | Accepted | 2024-02-20 |
        | ADR-003 | Implement CQRS pattern | Proposed | 2024-03-10 |

C4 Model — Architecture Visualization

C4 Model — иерархическая система диаграмм для описания архитектуры ПО на разных уровнях абстракции.

уровня C4

УровеньДиаграммаАудиторияДетализация
System ContextContext DiagramБизнес, архитекторыСистема + внешние actors
ContainersContainer DiagramАрхитекторы, разработчикиПриложения, БД, очереди
ComponentsComponent DiagramРазработчикиВнутренние компоненты
CodeClass DiagramРазработчикиКлассы, интерфейсы

C4-PlantUML — System Context Diagram

@startuml C4_Context
        !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml

        Person(customer, "Customer", "End user making payments")
        Person(supportAgent, "Support Agent", "Handles customer inquiries")

        System(paymentSystem, "Payment Platform", "Processes online payments securely")
        System_Ext(bankGateway, "Bank Gateway", "External banking API for transaction processing")
        System_Ext(notifications, "Notification Service", "Sends email/SMS notifications")

        Rel(customer, paymentSystem, "Makes payment", "HTTPS/REST")
        Rel(paymentSystem, bankGateway, "Processes transaction", "gRPC")
        Rel(paymentSystem, notifications, "Sends events", "AMQP")
        Rel(supportAgent, paymentSystem, "View transaction history", "HTTPS/UI")

        Legend()
        @enduml

C4-PlantUML — Container Diagram

@startuml C4_Container
        !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml

        Person(customer, "Customer")

        System_Boundary(paymentSystem, "Payment Platform") {
            Container(webApp, "Web Application", "ASP.NET Core 8.0", "API Gateway and frontend")
            Container(apiService, "Payment API", "ASP.NET Core 8.0", "Core payment processing")
            ContainerDb(database, "Transactions DB", "PostgreSQL 16", "Stores payment records")
            ContainerQueue(queue, "Message Queue", "RabbitMQ", "Async event processing")
            Container(cache, "Cache Layer", "Redis 7", "Session and rate limiting")
        }

        System_Ext(bankGateway, "Bank Gateway")

        Rel(customer, webApp, "Uses", "HTTPS")
        Rel(webApp, apiService, "Routes requests", "gRPC")
        Rel(apiService, database, "Reads/Writes", "TCP")
        Rel(apiService, cache, "Checks rate limit", "TCP")
        Rel(apiService, queue, "Publishes events", "AMQP")
        Rel(apiService, bankGateway, "Processes payment", "HTTPS/TLS")

        Legend()
        @enduml

C4-PlantUML — Component Diagram

@startuml C4_Component
        !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml

        Container_Boundary(apiService, "Payment API") {
            Component(controller, "PaymentController", "ASP.NET Core", "Handles HTTP requests")
            Component(service, "PaymentService", "Domain Service", "Business logic")
            Component(repo, "TransactionRepository", "EF Core", "Data access")
            Component(validator, "PaymentValidator", "FluentValidation", "Input validation")
            Component(notifier, "NotificationPublisher", "MassTransit", "Event publishing")
        }

        Rel(controller, validator, "Validates", "Input")
        Rel(controller, service, "Delegates", "ProcessPayment")
        Rel(service, repo, "Reads/Writes", "EF Core")
        Rel(service, notifier, "Publishes", "PaymentProcessed")

        Legend()
        @enduml

C4Sharp — .NET библиотека для C4 диаграмм

using C4Sharp.Diagrams;
        using C4Sharp.Diagrams.Plantuml;
        using C4Sharp.Diagrams.Themes;

        public class ContainerDiagramSample : ContainerDiagram
        {
            public ContainerDiagramSample()
            {
                Title = "Payment Platform - Containers";

                var customer = Person("customer", "Customer", "End user");

                var boundary = System_Boundary("paymentSystem", "Payment Platform");
                var webApp = boundary.AddContainer("webApp", "Web Application", "ASP.NET Core", "API Gateway");
                var apiService = boundary.AddContainer("apiService", "Payment API", "ASP.NET Core", "Core processing");
                var database = boundary.AddContainerDb("database", "Transactions DB", "PostgreSQL", "Data storage");
                var queue = boundary.AddContainer("queue", "Message Queue", "RabbitMQ", "Async events");

                customer.Uses(webApp, "HTTPS", "Makes payments");
                webApp.Uses(apiService, "gRPC", "Routes requests");
                apiService.Uses(database, "TCP", "Reads/Writes");
                apiService.Uses(queue, "AMQP", "Publishes events");
            }
        }

        // Compilation
        var diagrams = new DiagramBuilder[] { new ContainerDiagramSample() };
        new PlantumlContext()
            .UseDiagramImageBuilder()
            .Export("docs/diagrams", diagrams, new DefaultTheme());

Code Self-Documentation

Principles

PrincipleDescription
NamingИмена должны говорить "что", а не "как"
SRPКаждый класс/метод — одна ответственность
Small methodsМетод < 20 строк, одна логическая операция
Extract methodСложная логика → выделенный метод с понятным именем
Intention-revealingИмена методов описывают намерение, а не реализацию

Примеры

// Плохо: имя не говорит о намерении
        public void Process(int id, bool flag) { ... }

        // Хорошо: имя описывает намерение
        public void CancelExpiredOrders(int orderId) { ... }

        // Плохо: магические числа
        if (status == 3) { ... }

        // Хорошо: именованная константа
        if (status == OrderStatus.Shipped) { ... }

        // Плохо: сложная логика в одном методе
        public void ProcessOrder(Order order) {
            if (order.Total > 1000 && order.Customer.IsPremium && !order.IsCancelled) {
                // ... complex logic
            }
        }

        // Хорошо: extracted methods
        public void ProcessOrder(Order order) {
            if (!IsEligibleForDiscount(order)) return;
            ApplyDiscount(order);
            NotifyWarehouse(order);
        }

        private bool IsEligibleForDiscount(Order order)
            => order.Total > 1000 && order.Customer.IsPremium && !order.IsCancelled;

OpenAPI Descriptions

# openapi.yaml
        openapi: 3.0.3
        info:
          title: Payment Platform API
          version: 1.0.0
          description: API for processing online payments

        paths:
          /payments:
            post:
              summary: Create a new payment
              description: Processes a payment request and returns the payment details
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      $ref: '#/components/schemas/PaymentRequest'
              responses:
                '201':
                  description: Payment created successfully
                  content:
                    application/json:
                      schema:
                        $ref: '#/components/schemas/PaymentResponse'
                '400':
                  description: Invalid request
                '422':
                  description: Payment processing failed

        components:
          schemas:
            PaymentRequest:
              type: object
              required:
                - amount
                - currency
                - cardToken
              properties:
                amount:
                  type: number
                  format: decimal
                  description: Payment amount
                  example: 99.99
                currency:
                  type: string
                  description: ISO 4217 currency code
                  example: USD
                cardToken:
                  type: string
                  description: Tokenized card reference

Checklist для Documentation Practices

  • [ ] XML docs для всех public API (summary, param, returns, exception)
  • [ ] <example> с рабочим кодом для ключевых методов
  • [ ] <remarks> для нетривиального поведения
  • [ ] <typeparam> для generic типов и методов
  • [ ] <seealso> для связанных элементов
  • [ ] README с Quick Start и Architecture ссылками
  • [ ] ADR для значимых архитектурных решений
  • [ ] C4 Context Diagram для системы
  • [ ] C4 Container Diagram для архитектуры
  • [ ] C4 Component Diagram для ключевых сервисов
  • [ ] OpenAPI spec для REST API
  • [ ] Имена методов и классов self-documenting
  • [ ] No magic numbers — named constants/enums
  • [ ] Extract method для сложной логики
  • [ ] <GenerateDocumentationFile>true</GenerateDocumentationFile> в .csproj

Ссылки

  • [Microsoft Learn: XML Documentation Comments](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/)
  • [C4 Model — Simon Brown](https://c4model.com/)
  • [C4-PlantUML на GitHub](https://github.com/plantuml-stdlib/C4-PlantUML)
  • [C4Sharp — .NET библиотека для C4](https://github.com/JG0n/c4sharp)
  • [Structurizr .NET extensions](https://github.com/structurizr/dotnet-extensions)

Практика


Static Analysis Pipeline

SonarQube — Quality Gates и Security

Архитектура анализа

CI Pipeline
        ├── 1. Begin (SonarScanner begin)
        │   ├── Загрузка Quality Profile
        │   ├── Инъекция Roslyn analyzers в MSBuild
        │   └── Конфигурация проекта
        ├── 2. Build (dotnet build)
        │   ├── Компиляция с analyzers
        │   └── Генерация данных анализа
        ├── 3. Test (dotnet test)
        │   ├── Запуск тестов
        │   └── Генерация coverage отчёта (Coverlet)
        ├── 4. End (SonarScanner end)
        │   ├── Сбор данных анализа
        │   ├── Импорт coverage
        │   └── Загрузка результатов в SonarQube
        └── 5. Quality Gate
            ├── Проверка условий
            └── Blocking policy

Azure DevOps Pipeline

# azure-pipelines.yml
        trigger:
          branches:
            include:
              - main
              - 'release/*'
          paths:
            exclude:
              - '*.md'

        pool:
          vmImage: 'ubuntu-latest'

        steps:
          # 1. Prepare Analysis
          - task: SonarQubePrepare@8
            inputs:
              SonarQube: 'SonarQube-Connection'
              scannerMode: 'dotnet'
              projectKey: 'my-org.payment-platform'
              projectName: 'Payment Platform'
              projectVersion: $(Build.BuildNumber)
              extraProperties: |
                sonar.exclusions=**/Tests/**,**/Mock*.cs,**/Generated*.cs
                sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/TestResults/coverage.opencover.xml

          # 2. Build
          - task: DotNetCoreCLI@2
            displayName: 'dotnet build'
            inputs:
              command: 'build'
              arguments: '--configuration Release --no-incremental'

          # 3. Test + Coverage
          - task: DotNetCoreCLI@2
            displayName: 'dotnet test'
            inputs:
              command: 'test'
              arguments: '--configuration Release --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover'
              testRunTitle: 'Unit Tests + Coverage'

          # 4. Run Code Analysis
          - task: SonarQubeAnalyze@8

          # 5. Publish Quality Gate Result
          - task: SonarQubePublish@8
            inputs:
              pollingTimeoutSec: '300'

GitHub Actions

# .github/workflows/sonar-analysis.yml
        name: SonarQube Analysis
        on:
          push:
            branches: [ main, 'release/*' ]
          pull_request:
            branches: [ main ]

        jobs:
          sonarqube:
            runs-on: ubuntu-latest
            steps:
              - uses: actions/checkout@v4
                with:
                  fetch-depth: 0  # Required for PR analysis

              - name: Setup .NET
                uses: actions/setup-dotnet@v4
                with:
                  dotnet-version: '8.0.x'

              - name: Install SonarScanner
                run: dotnet tool install --global dotnet-sonarscanner

              - name: Begin SonarQube Scan
                run: |
                  dotnet sonarscanner begin \
                    /k:"my-org.payment-platform" \
                    /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \
                    /d:sonar.host.url="https://sonarcloud.io" \
                    /d:sonar.qualitygate.wait=true \
                    /d:sonar.cs.opencover.reportsPaths=$(pwd)/TestResults/coverage.opencover.xml

              - name: Build
                run: dotnet build --configuration Release

              - name: Test + Coverage
                run: |
                  dotnet test --configuration Release \
                    --collect:"XPlat Code Coverage" \
                    -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover

              - name: End SonarQube Scan
                run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"

Quality Gate Conditions

{
          "name": "Enterprise Quality Gate",
          "conditions": [
            { "metric": "coverage", "op": "GE", "threshold": 80 },
            { "metric": "new_coverage", "op": "GE", "threshold": 80 },
            { "metric": "reliability_rating", "op": "LE", "threshold": 1 },
            { "metric": "new_reliability_rating", "op": "LE", "threshold": 1 },
            { "metric": "security_rating", "op": "LE", "threshold": 1 },
            { "metric": "new_security_rating", "op": "LE", "threshold": 1 },
            { "metric": "maintainability_rating", "op": "LE", "threshold": 1 },
            { "metric": "new_maintainability_rating", "op": "LE", "threshold": 1 },
            { "metric": "alert_status", "op": "NE", "threshold": "ERROR" },
            { "metric": "new_alert_status", "op": "NE", "threshold": "ERROR" },
            { "metric": "duplicated_lines_density", "op": "LE", "threshold": 3 },
            { "metric": "complexity", "op": "LE", "threshold": 500 },
            { "metric": "ncloc", "op": "GT", "threshold": 0 }
          ]
        }

Code Coverage — Coverlet + ReportGenerator

Coverlet — сбор coverage

<!-- TestProject.csproj -->
        <Project Sdk="Microsoft.NET.Sdk">
          <PropertyGroup>
            <TargetFramework>net8.0</TargetFramework>
            <ImplicitUsings>enable</ImplicitUsings>
            <Nullable>enable</Nullable>
            <IsPackable>false</IsPackable>
            <CollectCoverage>true</CollectCoverage>
            <CoverletOutputFormat>cobertura</CoverletOutputFormat>
            <CoverletOutput>$(MSBuildThisFileDirectory)TestResults/coverage</CoverletOutput>
            <CoverletMergeWith>$(MSBuildThisFileDirectory)TestResults/coverage.json</CoverletMergeWith>
            <Exclude>[*]*.Tests.*,[*]*.Mocks.*</Exclude>
          </PropertyGroup>

          <ItemGroup>
            <PackageReference Include="coverlet.msbuild" Version="6.0.2">
              <PrivateAssets>all</PrivateAssets>
              <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
            </PackageReference>
          </ItemGroup>
        </Project>

RunSettings для Coverlet

<!-- Coverage.runsettings -->
        <?xml version="1.0" encoding="utf-8"?>
        <RunSettings>
          <DataCollectionRunSettings>
            <DataCollectors>
              <DataCollector friendlyName="XPlat code coverage">
                <Configuration>
                  <Format>cobertura,json,opencover</Format>
                  <Exclude>[MyApp.Web]*,[MyApp.Tests]*,[*]Moq*</Exclude>
                  <Include>[MyApp]*</Include>
                  <ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute>
                  <ExcludeByFile>**/Migrations/**/*.cs,**/Generated/**/*.cs</ExcludeByFile>
                  <IncludeTestAssembly>false</IncludeTestAssembly>
                  <SkipAutoProps>true</SkipAutoProps>
                  <UseSourceLink>true</UseSourceLink>
                </Configuration>
              </DataCollector>
            </DataCollectors>
          </DataCollectionRunSettings>
        </RunSettings>

ReportGenerator — генерация отчётов

# Установка
        dotnet tool install -g dotnet-reportgenerator-globaltool

        # Генерация HTML отчёта
        reportgenerator \
          -reports:"**/TestResults/coverage.cobertura.xml" \
          -targetdir:"./coverage-report" \
          -reporttypes:HtmlInline_AzurePipelines_Dark;Cobertura;JsonSummary \
          -sourcedirectories:"./src" \
          -classfilters:"-*Tests;*Mocks" \
          -assemblyfilters:"-*Tests;-*Mocks"

CI Pipeline с Coverage

# azure-pipelines.yml — coverage step
        - task: DotNetCoreCLI@2
          displayName: 'dotnet test'
          inputs:
            command: 'test'
            arguments: >
              --configuration Release
              --no-build
              --collect:"XPlat Code Coverage"
              -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura,json,opencover
            testRunTitle: 'Tests + Coverage'

        - task: UseDotNet@2
          inputs:
            packageType: 'command'
            command: 'custom'
            custom: 'tool install -g dotnet-reportgenerator-globaltool'

        - script: |
            reportgenerator \
              -reports:"$(Build.SourcesDirectory)/**/TestResults/coverage.cobertura.xml" \
              -targetdir:"$(Build.ArtifactStagingDirectory)/coverage-report" \
              -reporttypes:"HtmlInline_AzurePipelines_Dark;Cobertura;JsonSummary;MarkdownSummary" \
              -classfilters:"-*.Tests;-*.Mocks;-*.Generated" \
              -assemblyfilters:"-*.Tests;-*.Mocks;-*.Generated"
          displayName: 'Generate Coverage Report'

        - task: PublishBuildArtifacts@1
          inputs:
            PathtoPublish: '$(Build.ArtifactStagingDirectory)/coverage-report'
            ArtifactName: 'coverage-report'
            publishLocation: 'Container'

Mutation Testing — Stryker.NET

Конфигурация

// stryker-config.json
        {
          "stryker-config": {
            "solution": "../PaymentPlatform.sln",
            "test-project": "PaymentPlatform.Tests/PaymentPlatform.Tests.csproj",
            "mutate": [
              "**/*Service.cs",
              "!**/*Tests.cs",
              "!**/Mock*.cs",
              "!**/Generated*.cs"
            ],
            "mutation-level": "Standard",
            "reporters": [
              "html",
              "progress",
              "dashboard",
              "markdown"
            ],
            "concurrency": 4,
            "thresholds": {
              "high": 80,
              "low": 60,
              "break": 70
            },
            "test-runner": "vstest",
            "dashboard": {
              "project": "payment-platform",
              "version": "$(Build.SourceBranchName)"
            }
          }
        }

CI Pipeline с Stryker

# azure-pipelines.yml — mutation testing
        - job: MutationTesting
          pool:
            vmImage: 'ubuntu-latest'
          steps:
            - task: DotNetCoreCLI@2
              displayName: 'Install Stryker'
              inputs:
                command: 'custom'
                custom: 'tool'
                arguments: 'install dotnet-stryker --tool-path $(Agent.BuildDirectory)/tools'

            - task: DotNetCoreCLI@2
              displayName: 'Restore packages'
              inputs:
                command: 'restore'
                projects: 'PaymentPlatform.sln'

            - task: Powershell@2
              displayName: 'Run Stryker'
              inputs:
                targetType: 'inline'
                pwsh: true
                script: |
                  $(Agent.BuildDirectory)/tools/dotnet-stryker `
                    --project PaymentPlatform.Tests/PaymentPlatform.Tests.csproj `
                    --reporter "html" `
                    --reporter "dashboard" `
                    --reporter "markdown" `
                    --break-at 70 `
                    --with-baseline:$(System.PullRequest.TargetBranch) `
                    --dashboard-api-key $(StrykerDashboardApiKey) `
                    --version $(System.PullRequest.SourceBranch)

            - task: PublishMutationReport@1
              displayName: 'Publish Mutation Report'
              inputs:
                reportPattern: '**/StrykerOutput/**/mutation-report.html'

GitHub Actions с Stryker

# .github/workflows/mutation-testing.yml
        name: Mutation Testing
        on:
          push:
            branches: [ main ]
          pull_request:
            branches: [ main ]

        jobs:
          mutation:
            runs-on: ubuntu-latest
            steps:
              - uses: actions/checkout@v4
                with:
                  fetch-depth: 0

              - name: Setup .NET
                uses: actions/setup-dotnet@v4
                with:
                  dotnet-version: '8.0.x'

              - name: Install Stryker
                run: dotnet tool install -g dotnet-stryker

              - name: Run Stryker
                working-directory: ./tests/PaymentPlatform.Tests
                run: |
                  dotnet stryker \
                    --reporter "html" \
                    --reporter "dashboard" \
                    --break-at 80
                env:
                  STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}

              - name: Upload Report
                uses: actions/upload-artifact@v4
                if: always()
                with:
                  name: mutation-report
                  path: '**/StrykerOutput/**/mutation-report.html'

Security Scanning

NuGet Audit (встроенный в .NET 8+)

<!-- Directory.Build.props -->
        <PropertyGroup>
          <NuGetAudit>true</NuGetAudit>
          <NuGetAuditMode>all</NuGetAuditMode>
        </PropertyGroup>
<!-- CI: Treat audit warnings as errors -->
        <PropertyGroup Condition="'$(CI)' == 'true'">
          <NuGetAudit>true</NuGetAudit>
          <WarningsAsErrors>NU1900;NU1901;NU1902;NU1903;NU1904;NU1905</WarningsAsErrors>
        </PropertyGroup>

OWASP Dependency-Check

# azure-pipelines.yml — dependency check
        - task: OWASP-Dependency-Check-Security@1
          inputs:
            projectId: 'payment-platform'
            analyzerOptions: '--disableNodeAnalyzer --disableRetireJSAnalyzer'
            failBuildOnCVSS: '7'
            suppressFile: '$(Build.SourcesDirectory)/dependency-suppressions.xml'
            useAllTools: true

Snyk

# .github/workflows/snyk-security.yml
        name: Snyk Security Scan
        on:
          push:
            branches: [ main ]

        jobs:
          snyk:
            runs-on: ubuntu-latest
            steps:
              - uses: actions/checkout@v4

              - name: Setup .NET
                uses: actions/setup-dotnet@v4
                with:
                  dotnet-version: '8.0.x'

              - name: Restore dependencies
                run: dotnet restore PaymentPlatform.sln

              - name: Run Snyk to check for vulnerabilities
                uses: snyk/actions/dotnet@master
                env:
                  SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
                with:
                  args: --severity-threshold=high --all-projects

              - name: Upload SARIF to GitHub Code Scanning
                uses: github/codeql-action/upload-sarif@v3
                if: always()
                with:
                  sarif_file: snyk.sarif

Dependency Suppressions

<!-- dependency-suppressions.xml -->
        <?xml version="1.0" encoding="UTF-8"?>
        <suppressions>
          <suppress>
            <notes>False positive: Log4j vulnerability not applicable</notes>
            <packageUrl regex="true">^pkg:nuslog/log4net@.*$</packageUrl>
            <cve>CVE-2021-44228</cve>
          </suppress>
          <suppress>
            <notes>Transitive dependency, not directly used</notes>
            <packageUrl regex="true">^pkg:nuslNewtonsoft\.Json@12\.0\.3$</packageUrl>
            <cve>CVE-2024-12345</cve>
          </suppress>
        </suppressions>

Dependency Analysis

Unused Packages Detection

# NuGet package dependency analysis
        dotnet list package --include-transitive

        # Check for vulnerable packages
        dotnet list package --vulnerable --include-transitive

        # Compare with lock file
        dotnet outdated PaymentPlatform.sln

Automated Update Recommendations

# dependabot.yml
        version: 2
        updates:
          - package-ecosystem: "nuget"
            directory: "/"
            schedule:
              interval: "weekly"
            open-pull-requests-limit: 10
            reviewers:
              - "team-backend"
            labels:
              - "dependencies"
              - "security"
            commit-message:
              prefix: "deps"
            # Security updates only
            allow:
              - dependency-type: "direct"
            # Ignore specific packages
            ignore:
              - dependency-name: "Newtonsoft.Json"
                versions: ["13.0.0"]

Полный CI/CD Pipeline

# azure-pipelines.yml — полный pipeline
        name: $(Build.DefinitionName)_$(Date:yyyyMMdd)$(Rev:.r)

        trigger:
          branches:
            include: [ main, 'release/*' ]
          paths:
            exclude: [ '*.md' ]

        pr:
          branches:
            include: [ main ]

        variables:
          BuildConfiguration: 'Release'
          SonarOrganization: 'my-org'

        stages:
          - stage: BuildAndAnalyze
            jobs:
              - job: Build
                pool:
                  vmImage: 'ubuntu-latest'
                steps:
                  - task: UseDotNet@2
                    inputs:
                      version: '8.0.x'

                  - task: SonarQubePrepare@8
                    inputs:
                      SonarQube: 'SonarQube-Connection'
                      scannerMode: 'dotnet'
                      projectKey: 'my-org.payment-platform'
                      projectName: 'Payment Platform'
                      projectVersion: $(Build.BuildNumber)
                      extraProperties: |
                        sonar.exclusions=**/Tests/**,**/Mock*.cs
                        sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/TestResults/coverage.opencover.xml

                  - task: DotNetCoreCLI@2
                    inputs:
                      command: 'build'
                      arguments: '--configuration $(BuildConfiguration) --no-incremental'

                  - task: DotNetCoreCLI@2
                    inputs:
                      command: 'test'
                      arguments: '--configuration $(BuildConfiguration) --collect:"XPlat Code Coverage"'

                  - task: SonarQubeAnalyze@8
                  - task: SonarQubePublish@8
                    inputs:
                      pollingTimeoutSec: '300'

          - stage: Security
            dependsOn: BuildAndAnalyze
            condition: succeeded()
            jobs:
              - job: DependencyCheck
                steps:
                  - task: OWASP-Dependency-Check-Security@1
                    inputs:
                      failBuildOnCVSS: '7'

              - job: SnykScan
                steps:
                  - task: DotNetCoreCLI@2
                    inputs:
                      command: 'custom'
                      custom: 'tool'
                      arguments: 'install -g dotnet-stryker'

                  - task: DotNetCoreCLI@2
                    inputs:
                      command: 'test'
                      projects: 'PaymentPlatform.Tests'

                  - task: DotNetCoreCLI@2
                    inputs:
                      command: 'custom'
                      custom: 'stryker'
                      projects: 'PaymentPlatform.Tests'
                      arguments: '--break-at 70'

Checklist для Static Analysis Pipeline

  • [ ] SonarQube Begin → Build → Test → End → Quality Gate
  • [ ] Quality Gate: coverage ≥ 80%, reliability/security/maintainability = A
  • [ ] Coverlet: cobertura + opencover + json форматы
  • [ ] ReportGenerator: HTML + Cobertura + Markdown отчёты
  • [ ] Exclude: Tests, Mocks, Generated, Migrations
  • [ ] Stryker.NET: mutation testing с baseline comparison для PR
  • [ ] Thresholds: break-at 70%, high 80%, low 60%
  • [ ] NuGet Audit: включён в .NET 8+ проектах
  • [ ] OWASP Dependency-Check / Snyk: security scanning
  • [ ] Dependabot: automated dependency updates
  • [ ] Suppressions: документированные false positives
  • [ ] Pipeline: blocking policy на Quality Gate

Ссылки

  • [SonarQube .NET getting started](https://docs.sonarsource.com/sonarqube-server/2025.6/analyzing-source-code/dotnet-environments/getting-started-with-net)
  • [Coverlet documentation](https://github.com/coverlet-coverage/coverlet)
  • [ReportGenerator documentation](https://github.com/danielpalme/ReportGenerator)
  • [Stryker.NET documentation](https://stryker-mutator.io/docs/stryker-net/)
  • [NuGet Audit documentation](https://learn.microsoft.com/en-us/nuget/concepts/auditing-packages)
  • [OWASP Dependency-Check](https://github.com/dependency-check/DependencyCheck)
  • [Snyk .NET integration](https://docs.snyk.io/developer-tools/snyk-ci-cd-integrations/github-actions-for-snyk-setup-and-checking-for-vulnerabilities/snyk-dotnet-action)

Практика


Code Review Practices

Checklist-based Review

Code Review Checklist

# Code Review Checklist

        ## Functionality
        - [ ] Changes match the PR description and acceptance criteria
        - [ ] All new and existing tests pass
        - [ ] Edge cases are handled (null, empty, boundary values)
        - [ ] Error handling is appropriate (graceful degradation)
        - [ ] No regression in existing behavior

        ## Security
        - [ ] No hardcoded secrets, keys, or credentials
        - [ ] Input validation on all external inputs
        - [ ] No SQL injection vulnerabilities (parameterized queries)
        - [ ] No sensitive data in logs or error messages
        - [ ] Authentication/authorization checks in place
        - [ ] PII/EUII handled correctly (encryption, masking)

        ## Performance
        - [ ] No N+1 query problems
        - [ ] No blocking async calls (.Result, .Wait())
        - [ ] Appropriate use of caching
        - [ ] No memory leaks (proper disposal, event unsubscription)
        - [ ] Database queries are optimized (indexes, projections)

        ## Design
        - [ ] Single Responsibility Principle followed
        - [ ] No God classes or methods (>20 lines, >3 params)
        - [ ] Consistent with existing patterns in the codebase
        - [ ] No over-engineering (YAGNI principle)
        - [ ] Interfaces are small and focused
        - [ ] Dependency Injection used correctly

        ## Code Quality
        - [ ] Code compiles without warnings
        - [ ] Follows team coding conventions (.editorconfig)
        - [ ] Meaningful variable and method names
        - [ ] No code duplication (DRY principle)
        - [ ] Comments explain "why", not "what"
        - [ ] XML documentation for public API

        ## Testing
        - [ ] Tests added for new functionality
        - [ ] Tests cover edge cases and error scenarios
        - [ ] Tests are deterministic (no flaky tests)
        - [ ] Test names describe behavior, not implementation
        - [ ] Mocks are used appropriately (no over-mocking)

        ## Documentation
        - [ ] README updated if needed
        - [ ] API documentation updated (OpenAPI, XML docs)
        - [ ] ADR updated for architectural changes
        - [ ] Migration scripts documented (if applicable)

Automated Pre-Review

CI Pre-Review Checks

# .github/workflows/pr-checks.yml
        name: PR Pre-Review Checks
        on:
          pull_request:
            branches: [ main ]

        jobs:
          pre-review:
            runs-on: ubuntu-latest
            steps:
              - uses: actions/checkout@v4

              - name: Setup .NET
                uses: actions/setup-dotnet@v4
                with:
                  dotnet-version: '8.0.x'

              # 1. Build verification
              - name: Build
                run: dotnet build --configuration Release --no-incremental

              # 2. Code style enforcement
              - name: Check code style
                run: dotnet format --verify-no-changes --severity warn

              # 3. Unit tests
              - name: Run tests
                run: dotnet test --configuration Release --no-build --verbosity minimal

              # 4. Code coverage
              - name: Check coverage threshold
                run: |
                  dotnet test --configuration Release \
                    --collect:"XPlat Code Coverage" \
                    -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura
                  coverage=$(cat **/coverage.cobertura.xml | grep -oP '"line-rate"\s*=\s*"\K[^"]+')
                  if (( $(echo "$coverage < 0.80" | bc -l) )); then
                    echo "Coverage $coverage is below 80% threshold"
                    exit 1
                  fi

              # 5. PR size check
              - name: Check PR size
                run: |
                  total_lines=$(gh pr diff ${{ github.event.pull_request.number }} \
                    | grep -c '^[+-]')
                  if [ "$total_lines" -gt 400 ]; then
                    echo "Warning: PR has $total_lines changed lines (recommended: < 400)"
                  fi

              # 6. Security scan
              - name: NuGet Audit
                run: dotnet list package --vulnerable --include-transitive

Branch Protection Rules

# GitHub branch protection (via API)
        # POST /repos/{owner}/{repo}/branches/{branch}/protection
        {
          "required_status_checks": {
            "strict": true,
            "contexts": [
              "pre-review / Build",
              "pre-review / Tests",
              "pre-review / Code Style",
              "SonarQube Analysis",
              "Mutation Testing"
            ]
          },
          "enforce_admins": true,
          "required_pull_request_reviews": {
            "dismiss_stale_reviews": true,
            "require_code_owner_reviews": true,
            "required_approving_review_count": 2,
            "require_last_push_approval": true
          },
          "restrictions": null,
          "required_conversation_resolution": true,
          "block_creations_of_fork_pull_requests": false
        }

PR Size Guidelines

Правила размера PR

MetricTargetRationale
Lines changed< 400 LOCReview quality drops significantly above this
Number of files< 15Focus on related changes
Number of commits< 10Logical grouping
Review time< 4 hours (first review)Fast feedback loop
Review cycles1-2 roundsIndicates clear requirements
Time to merge< 2 daysAvoid stale PRs

Стратегии для больших изменений

Feature Split Strategy:
        ├── PR #1: Data models + migrations
        │   └── Changes: entities, DTOs, DB schema
        ├── PR #2: Repository layer
        │   └── Changes: data access, interfaces
        ├── PR #3: Business logic
        │   └── Changes: services, domain logic
        ├── PR #4: API layer
        │   └── Changes: controllers, endpoints
        └── PR #5: Frontend/UI
            └── Changes: UI components, integration

PR Template

## Description
        <!-- What changed and why (not just what — reviewers see the diff) -->

        ## Type of Change
        - [ ] Bug fix
        - [ ] New feature
        - [ ] Breaking change
        - [ ] Documentation update
        - [ ] Refactoring (no functional changes)

        ## Testing
        - [ ] Unit tests added/updated
        - [ ] Integration tests added/updated
        - [ ] Manual testing performed
        - [ ] All tests pass locally

        ## Checklist
        - [ ] Code follows team conventions (.editorconfig)
        - [ ] No compiler warnings
        - [ ] Self-review completed
        - [ ] Related issues linked (Fixes #123)
        - [ ] Documentation updated
        - [ ] No debug code / console.logs / commented code

Constructive Feedback Patterns

Как давать обратную связь

ПаттернПримерПочему работает
Вопросы вместо утверждений"What happens if userId is null?"Автор может обосновать решение
Конкретика"Line 42: user.Name could be null"Чётко указывает на проблему
Примеры"Consider: user.Name ?? string.Empty"Показывает альтернативу
"We" вместо "You""We should handle..."Не персонализирует проблему
Nit: для мелочей"Nit: typo in comment"Показывает не-blocker
Объяснение "почему""This could cause N+1 queries"Автор понимает причину

Bad vs Good примеры

BAD: "This code is wrong. Fix it."
        GOOD: "This method has 45 lines and handles 6 different concerns. 
               Consider extracting the validation logic into a separate method."

        BAD: "You shouldn't use .Result here."
        GOOD: "Using `.Result` blocks the thread. Consider:
               `await response.Content.ReadAsStringAsync()`

        BAD: "Bad naming."
        GOOD: "Consider renaming `processData` to `CalculateOrderTotal` 
               to better reflect what the method does."

        BAD: "Add tests."
        GOOD: "This method has 5 conditional branches. 
               Can we add tests for each branch to ensure full coverage?"

Когда НЕ комментировать

СитуацияПочему
Прочие проблемы вне scope PRЗафиксить в отдельном issue/PR
Личные предпочтения без обоснованияНе blocker, автор может не согласиться
Код, который не ломает productionПредложить в отдельном рефакторинге
Стиль, который соответствует .editorconfig.editorconfig уже определяет стандарт

Blameless Post-Mortem

Структура post-mortem

# Post-Mortem: [Incident Title]

        ## Summary
        Brief description of what happened, when, and impact.

        ## Timeline
        | Time | Event |
        |------|-------|
        | 10:00 | First user reports error |
        | 10:05 | Alert triggered (PagerDuty) |
        | 10:15 | Investigation started |
        | 10:45 | Root cause identified |
        | 11:30 | Fix deployed |
        | 12:00 | Verified resolution |

        ## Root Cause
        What was the underlying cause? (5 Whys)

        1. Why did the deployment fail? → Memory leak in new service
        2. Why wasn't the leak detected? → No memory profiling in CI
        3. Why no memory profiling? → Not part of the onboarding checklist
        4. Why not in checklist? → Checklist created before microservices
        5. Why after microservices? → Team focus was on shipping features

        ## Impact
        - Duration: 75 minutes
        - Affected users: ~1,200
        - Revenue impact: $X,XXX

        ## Action Items
        | Action | Owner | Due Date | Priority |
        |--------|-------|----------|----------|
        | Add memory profiling to CI | @dev1 | 2024-03-15 | High |
        | Update deployment checklist | @dev2 | 2024-03-10 | Medium |
        | Add circuit breaker pattern | @dev3 | 2024-03-20 | High |

        ## Lessons Learned
        What did we learn? How do we prevent recurrence?

Training Materials

Code Review Training — Key Points

Для авторов PR
  1. Self-review — просмотрите свой diff перед отправкой
  2. Small PRs — разбивайте большие изменения
  3. Clear description — объясните "что" и "почему"
  4. Tests included — тесты в том же PR
  5. Responsive — отвечайте на комментарии быстро
Для ревьюеров
  1. Read the description — поймите контекст перед кодом
  2. Review every line — не пропускайте файлы
  3. Focus on design — логика, архитектура, безопасность
  4. Be constructive — предлагайте решения, не только критикуйте
  5. Approve confidently — не блокируйте за личные предпочтения
Метрики team health
MetricTargetWhy
Time to first review< 4 hoursFast feedback
Review cycles1-2 roundsClear requirements
PR size< 400 LOCBetter review quality
Time to merge< 2 daysVelocity
Review participationBalancedNo single point of failure

Ссылки

  • [Google Engineering Practices — Code Review](https://google.github.io/eng-practices/review/)
  • [Microsoft Code with Engineering Playbook](https://microsoft.github.io/code-with-engineering-playbook/code-reviews/)
  • [GitHub Staff Engineer — Effective Code Review](https://github.blog/developer-skills/github/how-to-review-code-effectively-a-github-staff-engineers-philosophy/)
  • [PR Best Practices Guide](https://inventivehq.com/blog/pull-request-best-practices-guide)

Практика


Refactoring Techniques

Safe Refactorings

Basic Refactorings (IDE-supported)

RefactoringОписаниеБезопасность
RenameПереименование символа с обновлением всех ссылок100% безопасно
Extract MethodВыделение блока кода в отдельный метод100% безопасно
Inline MethodЗамена вызова методом вместо тела100% безопасно
Move ClassПеремещение класса в другой namespace/assembly100% безопасно
Change SignatureИзменение параметров метода100% безопасно
Encapsulate FieldЗамена поля на property100% безопасно
Extract VariableЗамена выражения переменной100% безопасно
Introduce Parameter ObjectГруппировка параметров в объект100% безопасно

Extract Method

// До: метод делает слишком много
        public decimal CalculateOrderTotal(Order order)
        {
            // Validation
            if (order == null) throw new ArgumentNullException(nameof(order));
            if (order.Items == null || order.Items.Count == 0) 
                throw new ArgumentException("Order must have items");
            if (order.Customer == null) 
                throw new ArgumentNullException(nameof(order.Customer));

            // Subtotal calculation
            decimal subtotal = 0;
            foreach (var item in order.Items)
            {
                subtotal += item.Price * item.Quantity;
            }

            // Tax calculation
            decimal taxRate = GetTaxRate(order.Customer.Region);
            decimal tax = subtotal * taxRate;

            // Discount calculation
            decimal discount = 0;
            if (order.Customer.IsPremium) discount = subtotal * 0.1m;
            if (order.Items.Any(i => i.Category == "Clearance")) discount += 5.0m;

            // Shipping
            decimal shipping = subtotal > 100 ? 0 : 9.99m;

            // Final total
            return subtotal + tax - discount + shipping;
        }

        // После: выделенные методы
        public decimal CalculateOrderTotal(Order order)
        {
            ValidateOrder(order);
            var subtotal = CalculateSubtotal(order);
            var tax = CalculateTax(subtotal, order.Customer.Region);
            var discount = CalculateDiscount(subtotal, order);
            var shipping = CalculateShipping(subtotal);
            return subtotal + tax - discount + shipping;
        }

        private static void ValidateOrder(Order order)
        {
            ArgumentNullException.ThrowIfNull(order);
            if (order.Items == null || order.Items.Count == 0)
                throw new ArgumentException("Order must have items");
            ArgumentNullException.ThrowIfNull(order.Customer);
        }

        private static decimal CalculateSubtotal(Order order)
            => order.Items.Sum(item => item.Price * item.Quantity);

        private static decimal CalculateTax(decimal subtotal, string region)
            => subtotal * GetTaxRate(region);

        private static decimal CalculateDiscount(decimal subtotal, Order order)
        {
            decimal discount = 0;
            if (order.Customer.IsPremium) discount = subtotal * 0.1m;
            if (order.Items.Any(i => i.Category == "Clearance")) discount += 5.0m;
            return discount;
        }

        private static decimal CalculateShipping(decimal subtotal)
            => subtotal > 100 ? 0 : 9.99m;

Behavioral Preservation

Test Coverage как Safety Net

Refactoring Safety Pipeline:
        ┌─────────────────────────────────────────┐
        │ 1. Ensure test coverage for existing    │
        │    behavior (characterization tests)    │
        ├─────────────────────────────────────────┤
        │ 2. Run tests — все должны проходить     │
        ├─────────────────────────────────────────┤
        │ 3. Refactor in small steps              │
        │    (rename → extract → move → etc.)     │
        ├─────────────────────────────────────────┤
        │ 4. Run tests after EACH step            │
        ├─────────────────────────────────────────┤
        │ 5. If tests fail → revert step          │
        ├─────────────────────────────────────────┤
        │ 6. Repeat until refactoring complete    │
        └─────────────────────────────────────────┘

Characterization Tests для Legacy кода

// Legacy метод без тестов
        public class OrderProcessor
        {
            public decimal ProcessOrder(decimal amount, string region, bool isPremium)
            {
                // Старая сложная логика
                decimal result = amount;
                if (region == "US") result *= 1.08m;
                else if (region == "EU") result *= 1.20m;
                else result *= 1.05m;

                if (isPremium) result *= 0.9m;

                if (result > 1000) result -= 50;
                else if (result > 500) result -= 25;

                return Math.Round(result, 2);
            }
        }

        // Characterization test — фиксируем текущее поведение
        [TestClass]
        public class OrderProcessorCharacterizationTests
        {
            [TestMethod]
            public void ProcessOrder_US_NonPremium_100()
            {
                var processor = new OrderProcessor();
                var result = processor.ProcessOrder(100, "US", false);
                Assert.AreEqual(108.00m, result); // Фиксируем текущее значение
            }

            [TestMethod]
            public void ProcessOrder_EU_Premium_600()
            {
                var processor = new OrderProcessor();
                var result = processor.ProcessOrder(600, "EU", true);
                Assert.AreEqual(612.00m, result); // EU: 600*1.20*0.9 = 648, -25 = 623
            }

            [TestMethod]
            public void ProcessOrder_UK_Premium_1100()
            {
                var processor = new OrderProcessor();
                var result = processor.ProcessOrder(1100, "UK", true);
                Assert.AreEqual(1029.50m, result); // UK: 1100*1.05*0.9 = 1039.50, -50 = 989.50
            }
        }

        // После characterization tests — безопасный рефакторинг
        public class OrderProcessor
        {
            public decimal ProcessOrder(decimal amount, string region, bool isPremium)
            {
                var withTax = ApplyTax(amount, region);
                var withDiscount = ApplyPremiumDiscount(withTax, isPremium);
                var withVolumeDiscount = ApplyVolumeDiscount(withDiscount);
                return Math.Round(withVolumeDiscount, 2);
            }

            private static decimal ApplyTax(decimal amount, string region)
                => region switch
                {
                    "US" => amount * 1.08m,
                    "EU" => amount * 1.20m,
                    _ => amount * 1.05m
                };

            private static decimal ApplyPremiumDiscount(decimal amount, bool isPremium)
                => isPremium ? amount * 0.9m : amount;

            private static decimal ApplyVolumeDiscount(decimal amount)
                => amount > 1000 ? amount - 50 : amount > 500 ? amount - 25 : amount;
        }

Incremental Refactoring

Baby Steps Approach

Incremental Refactoring Steps:

        Step 1: Add tests for existing behavior
        Step 2: Extract small method (3-5 lines)
        Step 3: Run tests — verify
        Step 4: Extract another method
        Step 5: Run tests — verify
        ...
        Step N: Remove duplication
        Step N+1: Run tests — verify
        Step N+2: Rename for clarity
        Step N+3: Run tests — verify
        Step N+4: Repeat until clean

Пример: Постепенный рефакторинг

// Step 0: Исходный метод (50 строк)
        public void ProcessPayment(PaymentRequest request)
        {
            if (request == null) throw new ArgumentNullException(nameof(request));
            if (request.Amount <= 0) throw new ArgumentException("Invalid amount");
            if (string.IsNullOrWhiteSpace(request.CardNumber)) 
                throw new ArgumentException("Card number required");
            if (request.CardNumber.Length != 16) 
                throw new ArgumentException("Invalid card number length");

            var customer = _repository.GetCustomer(request.CustomerId);
            if (customer == null) throw new CustomerNotFoundException();
            if (!customer.IsActive) throw new InactiveCustomerException();

            var balance = _bankService.CheckBalance(customer.AccountId);
            if (balance < request.Amount) throw new InsufficientFundsException();

            var transaction = new Transaction
            {
                CustomerId = customer.Id,
                Amount = request.Amount,
                Currency = request.Currency,
                CardNumber = MaskCardNumber(request.CardNumber),
                CreatedAt = DateTime.UtcNow
            };

            _bankService.ChargeCard(request.CardNumber, request.Amount, request.Currency);
            _repository.SaveTransaction(transaction);
            _notificationService.SendConfirmation(customer.Email, request.Amount);

            _logger.LogInformation("Payment processed: {TransactionId}", transaction.Id);
        }

        // Step 1: Выделение валидации
        public void ProcessPayment(PaymentRequest request)
        {
            ValidatePaymentRequest(request);  // Extract Method

            var customer = _repository.GetCustomer(request.CustomerId);
            if (customer == null) throw new CustomerNotFoundException();
            if (!customer.IsActive) throw new InactiveCustomerException();

            var balance = _bankService.CheckBalance(customer.AccountId);
            if (balance < request.Amount) throw new InsufficientFundsException();

            var transaction = new Transaction
            {
                CustomerId = customer.Id,
                Amount = request.Amount,
                Currency = request.Currency,
                CardNumber = MaskCardNumber(request.CardNumber),
                CreatedAt = DateTime.UtcNow
            };

            _bankService.ChargeCard(request.CardNumber, request.Amount, request.Currency);
            _repository.SaveTransaction(transaction);
            _notificationService.SendConfirmation(customer.Email, request.Amount);

            _logger.LogInformation("Payment processed: {TransactionId}", transaction.Id);
        }

        private static void ValidatePaymentRequest(PaymentRequest request)
        {
            ArgumentNullException.ThrowIfNull(request);
            if (request.Amount <= 0) throw new ArgumentException("Invalid amount");
            if (string.IsNullOrWhiteSpace(request.CardNumber))
                throw new ArgumentException("Card number required");
            if (request.CardNumber.Length != 16)
                throw new ArgumentException("Invalid card number length");
        }

        // Step 2: Выделение проверки клиента
        public void ProcessPayment(PaymentRequest request)
        {
            ValidatePaymentRequest(request);
            var customer = GetAndValidateCustomer(request.CustomerId);  // Extract Method

            var balance = _bankService.CheckBalance(customer.AccountId);
            if (balance < request.Amount) throw new InsufficientFundsException();

            // ... остальное без изменений
        }

        private Customer GetAndValidateCustomer(Guid customerId)
        {
            var customer = _repository.GetCustomer(customerId);
            if (customer == null) throw new CustomerNotFoundException();
            if (!customer.IsActive) throw new InactiveCustomerException();
            return customer;
        }

        // Step 3: Выделение создания транзакции
        public void ProcessPayment(PaymentRequest request)
        {
            ValidatePaymentRequest(request);
            var customer = GetAndValidateCustomer(request.CustomerId);
            ValidateFunds(customer.AccountId, request.Amount);

            var transaction = CreateTransaction(request, customer);  // Extract Method

            _bankService.ChargeCard(request.CardNumber, request.Amount, request.Currency);
            _repository.SaveTransaction(transaction);
            _notificationService.SendConfirmation(customer.Email, request.Amount);

            _logger.LogInformation("Payment processed: {TransactionId}", transaction.Id);
        }

        private static Transaction CreateTransaction(PaymentRequest request, Customer customer)
            => new()
            {
                CustomerId = customer.Id,
                Amount = request.Amount,
                Currency = request.Currency,
                CardNumber = MaskCardNumber(request.CardNumber),
                CreatedAt = DateTime.UtcNow
            };

        // Step 4: Выделение записи и нотификации
        public void ProcessPayment(PaymentRequest request)
        {
            ValidatePaymentRequest(request);
            var customer = GetAndValidateCustomer(request.CustomerId);
            ValidateFunds(customer.AccountId, request.Amount);

            var transaction = CreateTransaction(request, customer);
            ProcessTransaction(transaction, customer);  // Extract Method
        }

        private void ProcessTransaction(Transaction transaction, Customer customer)
        {
            _bankService.ChargeCard(transaction.CardNumber, transaction.Amount, transaction.Currency);
            _repository.SaveTransaction(transaction);
            _notificationService.SendConfirmation(customer.Email, transaction.Amount);
            _logger.LogInformation("Payment processed: {TransactionId}", transaction.Id);
        }

Legacy Code Strategies

Seam Identification

Seam TypeDescriptionExample
Precondition SeamУсловие до вызова методаПроверка null перед вызовом
Modification SeamПерехват модификацииMock repository
Horizon SeamТочка, за которой код не написанExtension method
Wrapper SeamОбёртка вокруг вызоваDecorator pattern
Conditional SeamТочка условной логикиif/else分支

Characterization Test Pattern

// Legacy код, который нужно рефакторить
        public class ReportGenerator
        {
            private readonly string _basePath = @"C:\Reports";
            private readonly int _maxRows = 10000;

            public string GenerateReport(List<Order> orders)
            {
                var sb = new StringBuilder();
                sb.AppendLine("ID,Date,Amount,Customer");

                int count = 0;
                foreach (var order in orders)
                {
                    if (count >= _maxRows) break;
                    sb.AppendLine($"{order.Id},{order.Date:yyyy-MM-dd},{order.Amount},{order.CustomerName}");
                    count++;
                }

                var path = Path.Combine(_basePath, $"report_{DateTime.Now:yyyyMMdd}.csv");
                File.WriteAllText(path, sb.ToString());
                return path;
            }
        }

        // Characterization test — фиксируем поведение
        [TestClass]
        public class ReportGeneratorCharacterizationTests
        {
            [TestMethod]
            public void GenerateReport_With5Orders_CreatesCsvFile()
            {
                var generator = new ReportGenerator();
                var orders = new List<Order>
                {
                    new() { Id = 1, Date = new DateTime(2024, 1, 15), Amount = 100, CustomerName = "Alice" },
                    new() { Id = 2, Date = new DateTime(2024, 1, 16), Amount = 200, CustomerName = "Bob" }
                };

                var result = generator.GenerateReport(orders);

                // Фиксируем: файл создан
                Assert.IsTrue(File.Exists(result));
                var content = File.ReadAllText(result);
                Assert.IsTrue(content.Contains("1,2024-01-15,100,Alice"));
                Assert.IsTrue(content.Contains("2,2024-01-16,200,Bob"));
            }

            [TestMethod]
            public void GenerateReport_With15000Orders_LimitsTo10000()
            {
                var generator = new ReportGenerator();
                var orders = Enumerable.Range(1, 15000)
                    .Select(i => new Order { Id = i, Date = DateTime.UtcNow, Amount = i, CustomerName = $"User{i}" })
                    .ToList();

                var result = generator.GenerateReport(orders);
                var lines = File.ReadAllLines(result);

                // Фиксируем: максимум 10001 строка (header + 10000 данных)
                Assert.AreEqual(10001, lines.Length);
            }
        }

Anti-Patterns Detection

God Class

Signs of God Class:
        ├── > 500 lines of code
        ├── > 20 public methods
        ├── > 10 instance fields
        ├── Known by many other classes (high coupling)
        ├── Does many different things (low cohesion)
        └── Instantiated everywhere
// God Class — до рефакторинга
        public class UserController
        {
            private readonly DbContext _db;
            private readonly ILogger _logger;
            private readonly EmailService _email;
            private readonly PdfGenerator _pdf;
            private readonly CacheManager _cache;

            public User GetUser(int id) { /* 50 lines */ }
            public User CreateUser(UserDto dto) { /* 80 lines */ }
            public void SendWelcomeEmail(User user) { /* 30 lines */ }
            public byte[] GenerateUserPdf(User user) { /* 60 lines */ }
            public void InvalidateCache(int id) { /* 10 lines */ }
            public List<User> SearchUsers(string query) { /* 40 lines */ }
            public void UpdateProfile(int id, ProfileDto dto) { /* 70 lines */ }
            public void DeleteUser(int id) { /* 50 lines */ }
            public UserRole GetRole(int id) { /* 20 lines */ }
            // ... 15 more methods
        }

        // Refactored — Split Up God Class
        public class UserController
        {
            private readonly IUserService _userService;
            private readonly ISearchService _searchService;

            public User GetUser(int id) => _userService.GetUser(id);
            public User CreateUser(UserDto dto) => _userService.CreateUser(dto);
            public List<User> SearchUsers(string query) => _searchService.Search(query);
            public void DeleteUser(int id) => _userService.DeleteUser(id);
        }

        public class UserService : IUserService
        {
            private readonly DbContext _db;
            private readonly EmailService _email;

            public User GetUser(int id) { /* ... */ }
            public User CreateUser(UserDto dto) { /* ... */ }
            public void UpdateProfile(int id, ProfileDto dto) { /* ... */ }
            public void DeleteUser(int id) { /* ... */ }
            private void SendWelcomeEmail(User user) { /* ... */ }
        }

        public class UserPdfGenerator
        {
            private readonly PdfGenerator _pdf;
            public byte[] GeneratePdf(User user) { /* ... */ }
        }

        public class UserCacheManager
        {
            private readonly CacheManager _cache;
            public void Invalidate(int id) { /* ... */ }
        }

Feature Envy

// Feature Envy — метод больше интересуется чужим объектом
        public class OrderService
        {
            public decimal GetTotalWithDetails(Order order)
            {
                // Метод OrderService больше интересуется order.Items, 
                // order.Customer.IsPremium, order.ShippingMethod
                return order.Items.Sum(i => i.Price * i.Quantity)
                    * (order.Customer.IsPremium ? 0.9m : 1.0m)
                    + order.ShippingMethod.Cost;
            }
        }

        // Fix: Move Method to Order (ближе к данным)
        public class Order
        {
            public decimal CalculateTotal()
            {
                var subtotal = Items.Sum(i => i.Price * i.Quantity);
                var discount = Customer.IsPremium ? 0.9m : 1.0m;
                return subtotal * discount + ShippingMethod.Cost;
            }
        }

        public class OrderService
        {
            public decimal GetTotalWithDetails(Order order)
                => order.CalculateTotal();  // Delegate to object
        }

Long Parameter List

// Long Parameter List — 7 параметров
        public void CreateOrder(string customerId, string productName, 
            decimal quantity, string currency, string shippingAddress,
            string billingAddress, bool isGift) { /* ... */ }

        // Fix: Introduce Parameter Object
        public record CreateOrderRequest(
            string CustomerId,
            string ProductName,
            decimal Quantity,
            string Currency,
            string ShippingAddress,
            string BillingAddress,
            bool IsGift);

        public void CreateOrder(CreateOrderRequest request) { /* ... */ }

        // Или: Builder Pattern
        public class OrderBuilder
        {
            private string _customerId;
            private string _productName;
            private decimal _quantity;
            private string _currency;
            private string _shippingAddress;
            private string _billingAddress;
            private bool _isGift;

            public OrderBuilder WithCustomer(string id) { _customerId = id; return this; }
            public OrderBuilder WithProduct(string name) { _productName = name; return this; }
            public OrderBuilder WithQuantity(decimal qty) { _quantity = qty; return this; }
            public OrderBuilder WithCurrency(string cur) { _currency = cur; return this; }
            public OrderBuilder WithShipping(string addr) { _shippingAddress = addr; return this; }
            public OrderBuilder WithBilling(string addr) { _billingAddress = addr; return this; }
            public OrderBuilder AsGift() { _isGift = true; return this; }
            public Order Build() => new(_customerId, _productName, _quantity, _currency, 
                _shippingAddress, _billingAddress, _isGift);
        }

Refactoring Plan: Procedural → OOP

Пример миграции

// Step 0: Procedural код
        public class OrderProcessor
        {
            public decimal Process(Order order, string type)
            {
                decimal total = 0;

                switch (type)
                {
                    case "standard":
                        total = order.Items.Sum(i => i.Price * i.Quantity);
                        total += order.Weight * 2.5m;
                        if (total > 100) total -= 5;
                        break;

                    case "express":
                        total = order.Items.Sum(i => i.Price * i.Quantity) * 1.2m;
                        total += order.Weight * 5.0m;
                        break;

                    case "overnight":
                        total = order.Items.Sum(i => i.Price * i.Quantity) * 1.5m;
                        total += 50;
                        break;
                }

                return Math.Round(total, 2);
            }
        }

        // Step 1: Introduce Strategy Pattern
        public interface ShippingStrategy
        {
            decimal Calculate(Order order);
        }

        public class StandardShipping : ShippingStrategy
        {
            public decimal Calculate(Order order)
            {
                var total = order.Items.Sum(i => i.Price * i.Quantity);
                total += order.Weight * 2.5m;
                if (total > 100) total -= 5;
                return Math.Round(total, 2);
            }
        }

        public class ExpressShipping : ShippingStrategy
        {
            public decimal Calculate(Order order)
            {
                var total = order.Items.Sum(i => i.Price * i.Quantity) * 1.2m;
                total += order.Weight * 5.0m;
                return Math.Round(total, 2);
            }
        }

        public class OvernightShipping : ShippingStrategy
        {
            public decimal Calculate(Order order)
            {
                var total = order.Items.Sum(i => i.Price * i.Quantity) * 1.5m;
                total += 50;
                return Math.Round(total, 2);
            }
        }

        // Step 2: Factory для выбора стратегии
        public static class ShippingStrategyFactory
        {
            private static readonly Dictionary<string, ShippingStrategy> _strategies = new()
            {
                { "standard", new StandardShipping() },
                { "express", new ExpressShipping() },
                { "overnight", new OvernightShipping() }
            };

            public static ShippingStrategy GetStrategy(string type)
                => _strategies.TryGetValue(type, out var strategy) 
                    ? strategy 
                    : throw new ArgumentException($"Unknown shipping type: {type}");
        }

        // Step 3: Refactored processor
        public class OrderProcessor
        {
            public decimal Process(Order order, string shippingType)
            {
                var strategy = ShippingStrategyFactory.GetStrategy(shippingType);
                return strategy.Calculate(order);
            }
        }

        // Step 4: Добавить новые типы без изменения Process
        public class PriorityShipping : ShippingStrategy
        {
            public decimal Calculate(Order order)
            {
                var total = order.Items.Sum(i => i.Price * i.Quantity) * 1.3m;
                total += order.Weight * 3.5m;
                return Math.Round(total, 2);
            }
        }

        // Step 5: Регистрация новой стратегии
        ShippingStrategyFactory.Register("priority", new PriorityShipping());

Checklist для Refactoring

  • [ ] Characterization tests написаны для legacy кода
  • [ ] Все существующие тесты проходят ДО рефакторинга
  • [ ] Refactoring выполнен в small steps
  • [ ] Tests запущены ПОСЛЕ каждого шага
  • [ ] No behavior changes — только внутренняя структура
  • [ ] God class разделён на классы с чёткой ответственностью
  • [ ] Feature envy устранён — методы ближе к данным
  • [ ] Long parameter list заменён на Parameter Object / Builder
  • [ ] Procedural → OOP через Strategy / Factory patterns
  • [ ] Code coverage не упала после рефакторинга

Ссылки

  • [Martin Fowler — Class Too Large](https://martinfowler.com/articles/class-too-large.html)
  • [Reengineering Patterns — Split Up God Class](https://eng.libretexts.org/Bookshelves/Computer_Science/Programming_and_Computation_Fundamentals/Book%3A_Object-Oriented_Reengineering_Patterns_(Demeyer_Ducasse_and_Nierstrasz)/09%3A_Redistribute_Responsibilities/9.04%3A_Split_Up_God_Class)
  • [Working Effectively with Legacy Code — Michael Feathers](https://www.oreilly.com/library/view/working-effectively-with/0131177052/)
  • [Refactoring.guru](https://refactoring.guru/)

Практика


Quality Metrics и Governance

Technical Debt Quantification

Technical Debt Model (SonarQube/SQuALE)

Technical Debt = Σ (Issue Remediation Cost × Issue Count)

        Remediation Cost = время в минутах на исправление каждого issue

Ключевые метрики

MetricKeyDescriptionTarget
Technical Debtsqale_indexСуммарные усилия для исправления всех maintainability issues< 5% of dev time
Technical Debt Ratiosqale_debt_ratioСоотношение debt / cost_to_develop< 5% (A), < 10% (B)
Maintainability Ratingsqale_ratingA (0-5%), B (6-10%), C (11-20%), D (21-50%), E (51%+)A или B
Code Smellscode_smellsКоличество maintainability issues< 10 per 1000 LOC

Rating Scale

Maintainability Rating:
        ┌──────────────────────────────────────────┐
        │ A (0-5%)    │ ≤ 5% tech debt ratio       │
        │ B (6-10%)   │ 6-10% tech debt ratio      │
        │ C (11-20%)  │ 11-20% tech debt ratio     │
        │ D (21-50%)  │ 21-50% tech debt ratio     │
        │ E (51%+)    │ > 50% tech debt ratio      │
        └──────────────────────────────────────────┘

Priority Ranking

// Приоритизация technical debt
        public class TechnicalDebtItem
        {
            public string RuleId { get; set; }
            public string File { get; set; }
            public int Line { get; set; }
            public decimal RemediationEffort { get; set; } // в минутах
            public string Severity { get; set; }
            public int Churn { get; set; } // частота изменений
            public int BugFixCorrelation { get; set; } // корреляция с багами

            // Priority Score = effort × churn × bug_correlation
            public decimal PriorityScore 
                => RemediationEffort * Churn * (1 + BugFixCorrelation * 0.5m);
        }

        // Сортировка по приоритету
        var debtItems = GetTechnicalDebtItems()
            .OrderByDescending(d => d.PriorityScore)
            .ToList();

Code Churn Analysis

Метрики Churn

MetricDescriptionTool
Churn CountКоличество коммитов, изменивших файлgit log
Churn VolumeСтроки добавлены/удаленыgit diff
Author EntropyShannon entropy авторов файлаgit log
Defect Correlation% коммитов — исправление баговgit log --grep
Co-changeФайлы, меняющиеся вместеgit log --name-only

Churn Analysis Tools

git-forensics (Node.js)
const { forensics } = require('git-forensics');

        // Анализ репозитория
        const results = await forensics.analyze({
          repo: '.',
          days: 90,
          thresholds: {
            hotspot: { warning: 80, critical: 95 },
            churn: { warning: 80, critical: 95 },
            ownership: { warning: 70, critical: 90, minAuthors: 4 }
          }
        });

        // Hotspots — файлы с наибольшим количеством изменений
        const hotspots = results.hotspots;

        // Churn — файлы с наибольшей волатильностью
        const churn = results.churn;

        // Coupling — скрытые зависимости
        const coupled = results.coupledPairs;

        // Ownership —knowledge silos
        const ownership = results.ownership;

        // Composite Risk Score (0-100)
        const riskScores = forensics.computeRiskScores(hotspots, churn, ownership);
churn_vs_complexity (Ruby/Python/JS/Java/Go)
# Hotspots ranking — файлы с высоким churn + complexity
        churn_vs_complexity --hotspots --csharp ./MyApp

        # Triage assessment — оценка риска файлов
        churn_vs_complexity --triage ./src/MyApp/Services

        # CI quality gate
        churn_vs_complexity --gate --max-gamma 25 ./src

        # Timetravel — отслеживание качества во времени
        churn_vs_complexity --csharp --timetravel 30 --since 2024-01-01 --graph ./MyApp > report.html

        # Diff comparison — сравнение здоровья кода между коммитами
        churn_vs_complexity --diff HEAD~10 --json ./MyApp

Code Churn Alerting

# .github/workflows/churn-alerting.yml
        name: Code Churn Analysis
        on:
          schedule:
            - cron: '0 0 * * 1'  # Every Monday
          workflow_dispatch:

        jobs:
          churn-analysis:
            runs-on: ubuntu-latest
            steps:
              - uses: actions/checkout@v4
                with:
                  fetch-depth: 0

              - name: Install git-forensics
                run: npm install -g git-forensics

              - name: Analyze churn
                run: |
                  git-forensics analyze --days 30 --output churn-report.json

              - name: Check thresholds
                run: |
                  jq '.hotspots[] | select(.percentiles.churn > 80) | {file, churn, riskScore}' churn-report.json

              - name: Create issue for high-churn modules
                if: failure()
                uses: actions/github-script@v7
                with:
                  script: |
                    const fs = require('fs');
                    const report = JSON.parse(fs.readFileSync('churn-report.json'));
                    const highChurn = report.hotspots.filter(h => h.percentiles.churn > 80);
            
                    if (highChurn.length > 0) {
                      const body = highChurn.map(h => 
                        `### ${h.file} (Risk: ${h.riskScore}, Churn: ${h.churn})`
                      ).join('\n');
              
                      github.rest.issues.create({
                        owner: context.repo.owner,
                        repo: context.repo.repo,
                        title: 'High Code Churn Alert',
                        body: `Modules with abnormally high churn detected:\n\n${body}`
                      });
                    }

Cyclomatic Complexity

Определение

Cyclomatic Complexity = 1 + number_of_decision_points

        Decision points: if, else if, while, for, foreach, case, catch, &&, ||, ?:

Интерпретация

ComplexityRatingAction
1-10LowИдеально
11-20ModerateПриемлемо, но стоит проверить
21-50HighТребуется рефакторинг
51+CriticalНемедленный рефакторинг

Когда высокая complexity оправдана

СценарийОбоснование
State MachineСостояния = decision points
Parser / CompilerGrammar rules = branches
Rule EngineBusiness rules = conditions
Generated CodeАвтоматически сгенерировано

Cognitive Complexity (альтернатива)

Cognitive Complexity учитывает понимаемость кода, а не только количество путей:

  • Nested structures штрафуются больше
  • Switch/case не увеличивает complexity (flat)
  • Logical operators (&&, ||) увеличивают

Maintainability Index

Формула

Maintainability Index = max(0, min(100, 
          (171 × 5/3 × ln(5/3) − 50 × ln(SLOC)) 
          − 24 × ln(Cyclomatic Complexity) 
          − 16 × ln(Comment Lines)
        ))
MI ScoreRatingDescription
100-25AHigh maintainability
24-10BModerate maintainability
9--10CLow maintainability
-11--50DVery low maintainability
<-51EUnmaintainable

Quality Trends

Tracking Improvement Over Time

# Quality Trend Dashboard Metrics
        metrics:
          - name: technical_debt_ratio
            source: sonarqube
            aggregation: weekly
            threshold:
              warning: 0.08
              critical: 0.15

          - name: code_coverage
            source: coverlet
            aggregation: weekly
            threshold:
              warning: 0.75
              critical: 0.60

          - name: mutation_score
            source: stryker
            aggregation: weekly
            threshold:
              warning: 0.70
              critical: 0.50

          - name: cyclomatic_complexity
            source: sonarqube
            aggregation: weekly
            threshold:
              warning: 15.0
              critical: 25.0

          - name: code_smells
            source: sonarqube
            aggregation: weekly
            threshold:
              warning: 50
              critical: 100

          - name: pr_size_avg
            source: github
            aggregation: weekly
            threshold:
              warning: 300
              critical: 500

Technical Debt Dashboard

Структура Dashboard

Technical Debt Dashboard
        ├── Overview
        │   ├── Technical Debt Ratio: 7.2% (B)
        │   ├── Total Debt: 340 hours
        │   ├── Debt per Sprint: ~42 hours
        │   └── Trend: ↓ 12% over 3 months
        │
        ├── Prioritized Remediation Plan
        │   ├── P0: Security vulnerabilities (50h)
        │   ├── P1: High-complexity methods (120h)
        │   ├── P2: Code smells (90h)
        │   └── P3: Documentation gaps (80h)
        │
        ├── Module Health
        │   ├── PaymentService: A (12 smells)
        │   ├── OrderProcessor: C (45 smells) ← needs attention
        │   ├── NotificationService: B (18 smells)
        │   └── ReportGenerator: D (78 smells) ← critical
        │
        ├── Churn Hotspots
        │   ├── src/Payment/Validator.cs: 23 commits in 30 days
        │   ├── src/Order/Processor.cs: 18 commits, 5 bug fixes
        │   └── src/Notification/Sender.cs: 15 commits, 3 authors
        │
        └── Quarterly Goals
            ├── Reduce tech debt ratio from 7.2% to 5%
            ├── Eliminate all E-rated modules
            ├── Achieve 85% code coverage
            └── Mutation score > 75%

Quarterly Quality Review Process

Process

Quarterly Quality Review:
        ┌─────────────────────────────────────────────────────────┐
        │ Month 1: Data Collection                                │
        │   ├── Pull metrics from SonarQube, Stryker, GitHub      │
        │   ├── Calculate trends (MoM, QoQ)                       │
        │   └── Generate quality report                           │
        ├─────────────────────────────────────────────────────────┤
        │ Month 1: Analysis                                       │
        │   ├── Identify worst-performing modules                 │
        │   ├── Calculate ROI for remediation                     │
        │   └── Prioritize remediation backlog                  │
        ├─────────────────────────────────────────────────────────┤
        │ Month 2: Planning                                       │
        │   ├── Create remediation sprint plan                    │
        │   ├── Assign owners for each item                       │
        │   └── Set quarterly targets                             │
        ├─────────────────────────────────────────────────────────┤
        │ Month 2-3: Execution                                    │
        │   ├── 10-15% of each sprint dedicated to tech debt      │
        │   ├── Track progress in backlog                         │
        │   └── Weekly status updates                             │
        ├─────────────────────────────────────────────────────────┤
        │ Month 4: Review & Report                                │
        │   ├── Measure improvement                               │
        │   ├── Update Q2 targets                                 │
        │   └── Present to stakeholders                           │
        └─────────────────────────────────────────────────────────┘

Quality Report Template

# Q1 2024 Quality Report

        ## Executive Summary
        Technical debt ratio improved from 9.1% to 7.2% (B rating).
        Code coverage increased from 72% to 81%.
        Mutation score: 68% → 74%.

        ## Key Metrics

        | Metric | Q4 2023 | Q1 2024 | Trend |
        |--------|---------|---------|-------|
        | Tech Debt Ratio | 9.1% | 7.2% | ↓ 21% |
        | Code Coverage | 72% | 81% | ↑ 13% |
        | Mutation Score | 68% | 74% | ↑ 9% |
        | Code Smells | 142 | 98 | ↓ 31% |
        | Avg PR Size | 420 LOC | 310 LOC | ↓ 26% |
        | Avg Review Time | 6.2h | 3.8h | ↓ 39% |

        ## Module Health

        | Module | Rating | Smells | Trend |
        |--------|--------|--------|-------|
        | PaymentService | A | 12 | → |
        | OrderProcessor | C | 45 | ↑ 8 |
        | NotificationService | B | 18 | ↓ 5 |
        | ReportGenerator | D | 78 | ↑ 15 |

        ## Remediation Progress
        - [x] Extracted OrderProcessor into 4 smaller classes
        - [x] Added characterization tests for ReportGenerator
        - [x] Implemented Stryker.NET in CI
        - [ ] Migrate ReportGenerator to component architecture
        - [ ] Reduce NotificationService complexity

        ## Q2 Goals
        1. Reduce tech debt ratio to 5% (A rating)
        2. Eliminate all D/E rated modules
        3. Achieve 85% code coverage
        4. Mutation score > 75%

Checklist для Quality Metrics и Governance

  • [ ] Technical debt tracked quarterly с trend analysis
  • [ ] Dashboard с prioritized remediation plan
  • [ ] Code churn alerting для frequently changing modules
  • [ ] Cyclomatic complexity monitored (threshold: warning 21, critical 50)
  • [ ] Maintainability Index tracked per module
  • [ ] Quarterly quality review process implemented
  • [ ] Quality trends visualized (MoM, QoQ)
  • [ ] Remediation plan с owners и deadlines
  • [ ] 10-15% sprint capacity for tech debt
  • [ ] Stakeholder reporting on quality improvements

Ссылки

  • [SonarQube: Understanding measures and metrics](https://docs.sonarsource.com/sonarqube-server/user-guide/code-metrics/metrics-definition.md)
  • [git-forensics — Git repository analysis](https://github.com/itaymendel/git-forensics)
  • [churn_vs_complexity — Code quality tool](https://github.com/beatmadsen/churn_vs_complexity)
  • [Drift — AI Code Coherence Monitor](https://mick-gsk.github.io/drift/reference/signals/tvs/)
  • [commit-prophet — Bug prediction from git](https://github.com/LakshmiSravyaVedantham/commit-prophet)

Практика


Developer Experience Platform

Project Templates

Custom dotnet new Template

Структура кастомного template:

my-enterprise-template/
        ├── .template.config/
        │   └── template.json          # Конфигурация template
        ├── content/
        │   └── {solution-name}/
        │       ├── Directory.Build.props
        │       ├── Directory.Packages.props
        │       ├── global.json
        │       ├── NuGet.config
        │       ├── .editorconfig
        │       ├── .gitignore
        │       ├── {solution-name}.slnx
        │       ├── src/
        │       │   ├── {solution-name}/
        │       │   │   ├── {solution-name}.csproj
        │       │   │   ├── Program.cs
        │       │   │   └── ...
        │       │   └── {solution-name}.Api/
        │       │       ├── {solution-name}.Api.csproj
        │       │       ├── Controllers/
        │       │       └── ...
        │       └── tests/
        │           ├── {solution-name}.Tests/
        │           └── {solution-name}.IntegrationTests/
        └── CHANGELOG.md

template.json

{
          "$schema": "http://json.schemastore.org/template",
          "author": "Engineering Team",
          "classifications": ["Web", "API", "Clean Architecture"],
          "description": "Enterprise .NET 8 template with Clean Architecture, testing, and CI/CD",
          "generatorVersions": "[8.0.0,)",
          "groupIdentity": "MyEnterprise.Template",
          "identity": "MyEnterprise.Template.CSharp",
          "name": "Enterprise .NET Application",
          "shortName": "enterprise",
          "sourceName": "{solution-name}",
          "tags": {
            "language": "C#",
            "type": "project"
          },
          "primaryOutputs": [
            { "path": "{solution-name}.slnx" }
          ],
          "postActions": [
            {
              "description": "Restore NuGet packages",
              "manualInstructions": [
                { "text": "dotnet restore" }
              ],
              "actionId": "3A7C4B45-8523-4364-9F0E-4B0C1A2D3E4F",
              "continueOnError": true
            }
          ]
        }

Directory.Build.props — Shared Defaults

<Project>
          <!-- General -->
          <PropertyGroup>
            <TargetFramework>net8.0</TargetFramework>
            <LangVersion>12.0</LangVersion>
            <Nullable>enable</Nullable>
            <Features>strict</Features>
            <ImplicitUsings>enable</ImplicitUsings>
          </PropertyGroup>

          <!-- Build -->
          <PropertyGroup>
            <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
            <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
            <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
            <UseArtifactsOutput>true</UseArtifactsOutput>
            <RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
          </PropertyGroup>

          <!-- Analysis -->
          <PropertyGroup>
            <AnalysisMode>AllEnabledByDefault</AnalysisMode>
            <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
            <RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis>
          </PropertyGroup>

          <!-- Testing -->
          <PropertyGroup Condition="'$(IsTestProject)' == 'true'">
            <CollectCoverage>true</CollectCoverage>
            <CoverletOutputFormat>cobertura</CoverletOutputFormat>
            <IsPackable>false</IsPackable>
          </PropertyGroup>

          <!-- Defaults -->
          <PropertyGroup>
            <IsPackable>false</IsPackable>
            <IsPublishable>false</IsPublishable>
            <IncludeSymbols>true</IncludeSymbols>
            <SymbolPackageFormat>snupkg</SymbolPackageFormat>
            <EmbedUntrackedSources>true</EmbedUntrackedSources>
          </PropertyGroup>
        </Project>

Central Package Management

<!-- Directory.Packages.props -->
        <Project>
          <PropertyGroup>
            <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
          </PropertyGroup>
          <ItemGroup>
            <PackageVersion Include="Microsoft.AspNetCore.App" Version="8.0.0" />
            <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
            <PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
            <PackageVersion Include="MediatR" Version="12.2.0" />
            <PackageVersion Include="FluentValidation" Version="11.9.0" />
            <PackageVersion Include="Serilog" Version="3.1.1" />
            <PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
            <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
            <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
            <PackageVersion Include="xunit" Version="2.6.6" />
            <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.6" />
            <PackageVersion Include="NSubstitute" Version="5.1.0" />
            <PackageVersion Include="FluentAssertions" Version="6.12.0" />
            <PackageVersion Include="coverlet.msbuild" Version="6.0.2" />
            <PackageVersion Include="MinVer" Version="5.0.0" />
          </ItemGroup>
        </Project>

Установка Template

# Install from local path
        dotnet new install ./my-enterprise-template

        # Install from NuGet
        dotnet new install MyEnterprise.Template

        # Create new project
        dotnet new enterprise -n MyProject

        # List installed templates
        dotnet new list

Local Development Environment

Reproducible Setup — devcontainer.json

{
          "name": ".NET Enterprise Dev",
          "image": "mcr.microsoft.com/devcontainers/dotnet:8.0",
          "features": {
            "ghcr.io/devcontainers/features/dotnet:2": {
              "version": "8.0",
              "sdkVariants": "true"
            },
            "ghcr.io/devcontainers/features/github-cli:1": {},
            "ghcr.io/devcontainers/features/docker-in-docker:2": {},
            "ghcr.io/devcontainers/features/node:1": {
              "version": "20"
            }
          },
          "customizations": {
            "vscode": {
              "extensions": [
                "ms-dotnettools.csharp",
                "ms-dotnettools.csdevkit",
                "ms-vscode.vscode-typescript-next",
                "eamodio.gitlens",
                "editorconfig.editorconfig",
                "redhat.vscode-yaml",
                "ms-azuretools.vscode-docker"
              ]
            }
          },
          "forwardPorts": [5000, 5001, 8080, 9000],
          "postCreateCommand": "dotnet restore && dotnet tool restore",
          "remoteEnv": {
            "DOTNET_CLI_TELEMETRY_OPTOUT": "1",
            "ASPNETCORE_ENVIRONMENT": "Development"
          }
        }

Docker Compose для Local Development

# docker-compose.yml
        services:
          api:
            build:
              context: .
              dockerfile: src/MyApp.Api/Dockerfile
            ports:
              - "5000:8080"
            environment:
              - ASPNETCORE_ENVIRONMENT=Development
              - ConnectionStrings__DefaultConnection=Host=db;Database=myapp;Username=postgres;Password=postgres
              - Redis__Configuration=redis:6379
            depends_on:
              db:
                condition: service_healthy
              redis:
                condition: service_healthy
              rabbitmq:
                condition: service_healthy

          db:
            image: postgres:16-alpine
            ports:
              - "5432:5432"
            environment:
              POSTGRES_DB: myapp
              POSTGRES_USER: postgres
              POSTGRES_PASSWORD: postgres
            volumes:
              - pgdata:/var/lib/postgresql/data
            healthcheck:
              test: ["CMD-SHELL", "pg_isready -U postgres"]
              interval: 5s
              timeout: 5s
              retries: 5

          redis:
            image: redis:7-alpine
            ports:
              - "6379:6379"
            healthcheck:
              test: ["CMD", "redis-cli", "ping"]
              interval: 5s
              timeout: 3s
              retries: 5

          rabbitmq:
            image: rabbitmq:3-management-alpine
            ports:
              - "5672:5672"
              - "15672:15672"
            environment:
              RABBITMQ_DEFAULT_USER: guest
              RABBITMQ_DEFAULT_PASS: guest
            healthcheck:
              test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
              interval: 10s
              timeout: 5s
              retries: 5

          sonarqube:
            image: sonarqube:lts
            ports:
              - "9000:9000"
            environment:
              SONAR_JDBC_URL: jdbc:postgresql://db-sonar:5432/sonar
              SONAR_JDBC_USERNAME: sonar
              SONAR_JDBC_PASSWORD: sonar
            depends_on:
              db-sonar:
                condition: service_healthy

          db-sonar:
            image: postgres:16-alpine
            environment:
              POSTGRES_DB: sonar
              POSTGRES_USER: sonar
              POSTGRES_PASSWORD: sonar
            volumes:
              - sonar-data:/var/lib/postgresql/data

        volumes:
          pgdata:
          sonar-data:

One-Command Provisioning

# scripts/provision.sh
        #!/bin/bash
        set -e

        echo "=== Provisioning Development Environment ==="

        # 1. Install .NET tools
        echo "Installing .NET tools..."
        dotnet tool restore

        # 2. Restore NuGet packages
        echo "Restoring NuGet packages..."
        dotnet restore

        # 3. Build
        echo "Building solution..."
        dotnet build --no-restore

        # 4. Run tests
        echo "Running tests..."
        dotnet test --no-build --verbosity minimal

        # 5. Start local services
        echo "Starting local services..."
        docker-compose up -d db redis rabbitmq

        # 6. Run database migrations
        echo "Applying database migrations..."
        dotnet ef database update --project src/MyApp.Infrastructure --startup-project src/MyApp.Api

        # 7. Seed data
        echo "Seeding database..."
        dotnet run --project src/MyApp.Seeder

        echo "=== Environment Ready ==="
        echo "API: http://localhost:5000"
        echo "Swagger: http://localhost:5000/swagger"
        echo "RabbitMQ Management: http://localhost:15672"
        echo "SonarQube: http://localhost:9000"

CI/CD Visibility

Developer Dashboard

# Developer Dashboard — API endpoints
        # GET /api/dashboard/developer/{userId}
        {
          "developer": {
            "id": "dev-001",
            "name": "Jane Smith",
            "avatar": "https://avatars..."
          },
          "metrics": {
            "prs_created_this_month": 12,
            "prs_merged_this_month": 10,
            "avg_review_time_hours": 3.2,
            "code_coverage_own_code": 84.5,
            "bugs_introduced": 2,
            "bugs_fixed": 8,
            "commits_this_month": 45
          },
          "quality_trend": {
            "tech_debt_ratio": 0.072,
            "coverage": 0.81,
            "mutation_score": 0.74,
            "code_smells": 98
          },
          "recent_activity": [
            { "type": "pr_merged", "title": "Add payment retry logic", "date": "2024-03-15" },
            { "type": "pr_reviewed", "title": "Review: Order validation", "date": "2024-03-14" },
            { "type": "bug_fixed", "title": "Fix: Null reference in NotificationService", "date": "2024-03-13" }
          ]
        }

GitHub Actions Dashboard

# .github/workflows/dashboard-update.yml
        name: Update Developer Dashboard
        on:
          pull_request:
            types: [closed]
          workflow_run:
            workflows: ["SonarQube Analysis"]
            types: [completed]

        jobs:
          update-dashboard:
            runs-on: ubuntu-latest
            steps:
              - name: Collect metrics
                uses: actions/github-script@v7
                with:
                  script: |
                    const pr = context.payload.pull_request;
                    if (!pr) return;

                    // Collect PR metrics
                    const metrics = {
                      author: pr.user.login,
                      size: pr.additions + pr.deletions,
                      reviewTime: (pr.merged_at - pr.created_at) / (1000 * 60 * 60),
                      comments: pr.comments,
                      approvals: pr.reviews.filter(r => r.state === 'APPROVED').length
                    };

                    // Update dashboard API
                    await fetch('https://dashboard.api/companies/eng/metrics', {
                      method: 'POST',
                      headers: { 'Authorization': `Bearer ${process.env.DASHBOARD_TOKEN}` },
                      body: JSON.stringify(metrics)
                    });

Self-Service Tools

Code Generation

# Custom dotnet CLI tool для генерации boilerplate
        # Install: dotnet tool install -g MyEnterprise.Cli

        # Создать новый entity + CRUD
        my-cli generate entity --name OrderItem --table order_items

        # Создать новый API endpoint
        my-cli generate controller --name Payments --actions Create,Get,Update

        # Создать тест для сервиса
        my-cli generate test --name OrderService --context unit

        # Создать migration
        my-cli generate migration --name AddCustomerSegments

Migration Helpers

// MyEnterprise.Cli — EF Core migration helper
        public class MigrationGenerator
        {
            public static void CreateMigration(string name)
            {
                var startupProject = FindStartupProject();
                var infrastructureProject = FindInfrastructureProject();

                var psi = new ProcessStartInfo
                {
                    FileName = "dotnet",
                    Arguments = $"ef migrations add {name} " +
                                $"--project {infrastructureProject} " +
                                $"--startup-project {startupProject}",
                    RedirectStandardOutput = true,
                    UseShellExecute = false
                };

                using var process = Process.Start(psi)!;
                process.WaitForExit();
            }
        }

Debugging Utilities

// Debugging middleware для local development
        public class DebuggingMiddleware
        {
            private readonly RequestDelegate _next;

            public DebuggingMiddleware(RequestDelegate next) => _next = next;

            public async Task InvokeAsync(HttpContext context, IServiceProvider services)
            {
                if (context.Request.Path.StartsWithSegments("/debug"))
                {
                    // Health check with dependencies
                    if (context.Request.Path == "/debug/health")
                    {
                        var dbHealth = await CheckDatabaseHealth(services);
                        var redisHealth = await CheckRedisHealth(services);
                        var rabbitHealth = await CheckRabbitMqHealth(services);

                        var health = new
                        {
                            status = dbHealth && redisHealth && rabbitHealth ? "healthy" : "degraded",
                            database = dbHealth,
                            redis = redisHealth,
                            rabbitmq = rabbitHealth,
                            timestamp = DateTime.UtcNow
                        };

                        await context.Response.WriteAsJsonAsync(health);
                        return;
                    }

                    // Performance metrics
                    if (context.Request.Path == "/debug/perf")
                    {
                        var perf = new
                        {
                            memory = GC.GetTotalMemory(false),
                            processTime = DateTime.UtcNow - Process.GetCurrentProcess().StartTime,
                            uptime = DateTime.UtcNow - Environment.GetCommandLineArgs()[0]
                        };

                        await context.Response.WriteAsJsonAsync(perf);
                        return;
                    }
                }

                await _next(context);
            }
        }

Checklist для Developer Experience Platform

  • [ ] Custom dotnet new template с best practices
  • [ ] Directory.Build.props с shared defaults
  • [ ] Central Package Management (Directory.Packages.props)
  • [ ] .editorconfig в template
  • [ ] devcontainer.json для reproducible environment
  • [ ] docker-compose.yml с local services
  • [ ] One-command provisioning script
  • [ ] CI/CD dashboard с personal metrics
  • [ ] Code generation CLI tools
  • [ ] Migration helpers
  • [ ] Debugging utilities (health checks, metrics)
  • [ ] Self-service onboarding для новых разработчиков

Ссылки

  • [Microsoft Learn: Create a project template for dotnet new](https://learn.microsoft.com/en-us/dotnet/core/tutorials/cli-templates-create-project-template)
  • [Setting up a .NET Project in 2024](https://www.ethan-shea.com/posts/setting-up-dotnet-2024)
  • [olstakh/dotnet-repo-template](https://github.com/olstakh/dotnet-repo-template)
  • [.NET CLI templates reference](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-new-sdk-templates)

Практика


Architecture Decision Records

MADR Format

MADR (Markdown Architectural Decision Records) — стандарт для документирования архитектурных решений.

Структура ADR

---
        status: "{proposed | rejected | accepted | deprecated | superseded}"
        date: {YYYY-MM-DD}
        decision-makers: {list}
        consulted: {list}
        informed: {list}
        ---

        # {short title, representative of solved problem and found solution}

        ## Context and Problem Statement

        {Describe the context and problem statement}

        ## Decision Drivers

        * {driver 1}
        * {driver 2}
        * {constraint}

        ## Considered Options

        * {option 1}
        * {option 2}
        * {option 3}

        ## Decision Outcome

        Chosen option: "{option}", because {justification}.

        ### Consequences

        * Good, because {positive consequence}
        * Bad, because {negative consequence}
        * Neutral, because {neutral consequence}

        ### Confirmation

        {How to verify the decision is implemented}

        ## Pros and Cons of the Options

        ### {option 1}

        * Good, because {argument}
        * Bad, because {argument}

        ### {option 2}

        * Good, because {argument}
        * Bad, because {argument}

        ## More Information

        {Additional context, links, references}

ADR Lifecycle

ADR Lifecycle:

        proposed → review → accepted → implemented → monitored
                                            ↓
                                      deprecated (superseded by new ADR)
                                            ↓
                                      superseded → archived
StatusОписание
proposedРешение предложено, на ревью
acceptedРешение одобрено стейкхолдерами
deprecatedРешение устарело, есть лучшее
supersededЗаменено другим ADR
rejectedОтклонено на этапе ревью

ADR Template

---
        status: proposed
        date: 2024-03-15
        decision-makers: [Jane Smith, John Doe]
        consulted: [Architecture Board, Security Team]
        informed: [Engineering Team, Product Owners]
        ---

        # ADR-0001: Use PostgreSQL as Primary Database

        ## Context and Problem Statement

        Our payment processing platform needs a reliable, ACID-compliant database to store transaction records. The system must support high write throughput (10,000 TPS), complex queries for reporting, and point-in-time recovery. Current system uses MySQL 5.7 which lacks native JSON support and has limited partitioning capabilities.

        ## Decision Drivers

        * **Reliability**: ACID compliance is mandatory for financial data
        * **Performance**: Must handle 10,000+ writes per second
        * **Scalability**: Horizontal scaling for growing transaction volume
        * **Ecosystem**: Rich .NET ORM support (EF Core)
        * **Cost**: Open-source preferred, minimal licensing costs
        * **Recovery**: Point-in-time recovery < 15 minutes RTO

        ## Considered Options

        * PostgreSQL 16
        * Microsoft SQL Server 2022
        * MongoDB 7.0
        * CockroachDB Serverless

        ## Decision Outcome

        Chosen option: "PostgreSQL 16", because it provides the best balance of ACID compliance, mature EF Core support, advanced partitioning, and cost-effectiveness for our use case.

        ### Consequences

        * Good, because EF Core has excellent PostgreSQL support
        * Good, because native JSONB allows flexible schema evolution
        * Good, because logical replication enables read replicas
        * Good, because open-source (PostgreSQL License)
        * Bad, because team has less PostgreSQL experience than MySQL
        * Bad, because certain window functions have performance issues at scale
        * Neutral, because connection pooling requires PgBouncer in production

        ### Confirmation

        * Migrate all Entity Framework migrations to PostgreSQL dialect
        * Performance benchmark: 10,000 TPS on read replicas
        * Security review: encryption at rest and in transit enabled

        ## Pros and Cons of the Options

        ### PostgreSQL 16

        * Good, because mature, battle-tested (25+ years)
        * Good, because excellent EF Core support
        * Good, because logical replication for read replicas
        * Good, because JSONB for flexible data
        * Good, because open-source
        * Bad, because requires PgBouncer for connection pooling at scale
        * Bad, because less familiar to team (MySQL background)

        ### Microsoft SQL Server 2022

        * Good, because excellent .NET integration
        * Good, because team has SQL Server experience
        * Good, because Azure SQL PaaS option
        * Bad, because licensing costs ($$$)
        * Bad, because less flexible for self-hosted deployments

        ### MongoDB 7.0

        * Good, because schema flexibility
        * Good, because horizontal scaling
        * Bad, because eventual consistency (not ACID for multi-document)
        * Bad, because EF Core support is limited
        * Bad, because not ideal for complex analytical queries

        ### CockroachDB Serverless

        * Good, because distributed by design
        * Good, because SQL interface
        * Bad, because managed service lock-in
        * Bad, because higher latency for local reads
        * Bad, because limited .NET ecosystem

        ## More Information

        * [PostgreSQL vs SQL Server for .NET](https://learn.microsoft.com/en-us/dotnet/standard/data/sql-server-vs-postgresql)
        * [EF Core PostgreSQL provider](https://github.com/npgsql/efcore.pg)
        * [PgBouncer connection pooling](https://www.pgbouncer.org/)

ADR: Message Broker Selection

---
        status: accepted
        date: 2024-02-20
        decision-makers: [Jane Smith, Alex Chen]
        consulted: [Platform Team, Security Team]
        informed: [Engineering Team]
        ---

        # ADR-0002: Adopt MassTransit for Distributed Messaging

        ## Context and Problem Statement

        The payment platform requires reliable asynchronous messaging between services (Payment Service, Notification Service, Fraud Detection). We need a message broker with guaranteed delivery, retry semantics, and dead letter queue support. Direct RabbitMQ client integration is being considered, but we want to evaluate higher-level abstractions.

        ## Decision Drivers

        * **Reliability**: Guaranteed delivery with retry and DLQ
        * **Abstraction**: Decouple from specific broker implementation
        * **Observability**: Built-in tracing and metrics
        * **Performance**: High throughput with low latency
        * **Team productivity**: Reduce boilerplate messaging code
        * **Cloud portability**: Support RabbitMQ, Azure Service Bus, AWS SQS

        ## Considered Options

        * MassTransit + RabbitMQ
        * Raw RabbitMQ.Client
        * MediatR + In-Memory
        * Azure Service Bus + Azure.Messaging.ServiceBus

        ## Decision Outcome

        Chosen option: "MassTransit + RabbitMQ", because it provides broker abstraction, built-in retry/DLQ, observability integration, and cloud portability while keeping RabbitMQ as the default broker.

        ### Consequences

        * Good, because single abstraction across multiple broker implementations
        * Good, because built-in retry policies, DLQ, and sagas
        * Good, because OpenTelemetry integration out of the box
        * Good, because reduces messaging boilerplate by ~60%
        * Bad, because additional dependency to learn and maintain
        * Bad, because RabbitMQ is required for local development
        * Neutral, because some advanced RabbitMQ features may not be exposed

        ### Confirmation

        * All inter-service communication uses MassTransit
        * Retry policy: 3 attempts with exponential backoff
        * DLQ configured for all queues
        * OpenTelemetry tracing enabled
        * Performance benchmark: 5,000 messages/sec per consumer

        ## Pros and Cons of the Options

        ### MassTransit + RabbitMQ

        * Good, because broker abstraction (swap RabbitMQ ↔ Azure SB ↔ SQS)
        * Good, because built-in retry, DLQ, sagas
        * Good, because OpenTelemetry integration
        * Good, because reduces boilerplate ~60%
        * Bad, because learning curve for advanced features
        * Bad, because RabbitMQ required locally

        ### Raw RabbitMQ.Client

        * Good, because full control over RabbitMQ features
        * Good, because no additional dependency
        * Bad, because boilerplate for retry, DLQ, error handling
        * Bad, because broker-locked (hard to switch)
        * Bad, because manual observability integration

        ### MediatR + In-Memory

        * Good, because simple for same-process communication
        * Bad, because no persistence (messages lost on restart)
        * Bad, because no cross-service communication
        * Bad,不适合 distributed architecture

        ### Azure Service Bus

        * Good, because managed PaaS
        * Good, because Azure-native integration
        * Bad, because vendor lock-in
        * Bad, because additional cost
        * Bad, because less control over broker configuration

        ## More Information

        * [MassTransit Documentation](https://masstransit.io/)
        * [RabbitMQ .NET Client](https://www.rabbitmq.com/dotnet)
        * [OpenTelemetry .NET](https://opentelemetry.io/docs/languages/net/)

ADR Workflow

Review Process

ADR Review Workflow:

        1. Author creates ADR in proposed status
           └── docs/architecture/ADRs/ADR-NNNN-title.md

        2. Author creates PR with ADR
           └── Requests review from decision-makers

        3. Reviewers evaluate:
           ├── Context is clear and complete
           ├── All options considered
           ├── Consequences understood
           └── Decision is justified

        4. Status updated to accepted/rejected
           └── Comments merged to code

        5. Implementation tracked:
           ├── Code review checks ADR compliance
           ├── Documentation updated
           └── ADR linked in PR description

        6. Monitor and revisit:
           ├── Quarterly review of all ADRs
           ├── Deprecate outdated decisions
           └── Supersede with new ADRs

ADR Search and Reference System

# docs/architecture/ADRs/README.md

        # Architecture Decision Records

        ## Quick Reference

        | ID | Title | Status | Date | Service |
        |----|-------|--------|------|---------|
        | ADR-0001 | Use PostgreSQL as Primary Database | accepted | 2024-01-15 | Platform |
        | ADR-0002 | Adopt MassTransit for Messaging | accepted | 2024-02-20 | Platform |
        | ADR-0003 | Implement CQRS Pattern | proposed | 2024-03-10 | Orders |
        | ADR-0004 | Use Redis for Caching | accepted | 2024-01-20 | Platform |
        | ADR-0005 | API Versioning Strategy | deprecated | 2024-02-01 | API |
        | ADR-0006 | Use URL Path Versioning | superseded by ADR-0007 | 2024-02-01 | API |
        | ADR-0007 | Use Query String Versioning | accepted | 2024-03-01 | API |

        ## Search

        ### By Status
        - **Proposed**: ADR-0003 (CQRS)
        - **Accepted**: ADR-0001, ADR-0002, ADR-0004, ADR-0007
        - **Deprecated**: ADR-0005
        - **Superseded**: ADR-0006

        ### By Service
        - **Platform**: ADR-0001, ADR-0002, ADR-0004
        - **Orders**: ADR-0003
        - **API**: ADR-0005, ADR-0006, ADR-0007

        ### By Date
        - **Q1 2024**: ADR-0001, ADR-0004, ADR-0005, ADR-0006, ADR-0007, ADR-0002, ADR-0003

        ## Links to Code

        | ADR | Related Code |
        |-----|-------------|
        | ADR-0001 | `src/MyApp.Infrastructure/Postgres/` |
        | ADR-0002 | `src/MyApp.Shared/Messaging/` |
        | ADR-0004 | `src/MyApp.Shared/Caching/` |
        | ADR-0007 | `src/MyApp.Api/Controllers/v1/` |

ADR Integration with Code

Linking ADRs to Code

// Attribute для traceability от кода к ADR
        [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
        public class ArchitectureDecisionAttribute : Attribute
        {
            public string AdrId { get; }
            public string Reason { get; }

            public ArchitectureDecisionAttribute(string adrId, string reason)
            {
                AdrId = adrId;
                Reason = reason;
            }
        }

        // Пример использования
        [ArchitectureDecision("ADR-0002", "MassTransit for distributed messaging")]
        public class PaymentConsumer : IConsumer<PaymentProcessed>
        {
            public async Task Consume(ConsumeContext<PaymentProcessed> context)
            {
                // Implementation using MassTransit
            }
        }

Code Review Checklist — ADR Compliance

## ADR Compliance Checklist

        - [ ] Does this change align with existing ADRs?
        - [ ] Does this change require a new ADR?
        - [ ] Are there conflicting ADRs that need resolution?
        - [ ] Is this a deviation from an accepted ADR? If so, propose deprecation.

Checklist для Architecture Decision Records

  • [ ] ADR template (MADR 4.0) adopted
  • [ ] ADR numbering convention (ADR-NNNN)
  • [ ] ADR statuses: proposed → accepted → deprecated → superseded
  • [ ] ADR review workflow с stakeholder approval
  • [ ] ADR search system (README index)
  • [ ] ADR linked to code (attributes, comments)
  • [ ] ADR compliance в code review checklist
  • [ ] Quarterly ADR review process
  • [ ] ADRs для всех major architectural decisions
  • [ ] ADRs для technology choices
  • [ ] ADRs для pattern selections
  • [ ] ADRs для tool selections

Ссылки

  • [adr/madr — Markdown Architectural Decision Records](https://github.com/adr/madr)
  • [MADR Template](https://github.com/adr/madr/blob/main/template/adr-template.md)
  • [MADR Examples](https://adr.github.io/madr/examples.html)
  • [ADR Community](https://adr.github.io/)
  • [Nygard's ADR Original Concept](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions)

Практика


Контрольная точка модуля 13

Проект: Quality governance framework для engineering organization
  • Custom Roslyn analyzers для team-specific rules
  • SonarQube quality gate в CI pipeline с blocking policy
  • Code review checklist и training materials
  • Technical debt dashboard с prioritized remediation plan
  • ADR process с template, workflow, и knowledge base
Критерии прохождения:
  • Zero critical code smells в production branches
  • All PRs pass automated quality checks before human review
  • Technical debt tracked и measured quarterly с improvement trend
  • ADR exists для каждого major architectural decision за последний квартал
  • Average PR review time < 24 hours с constructive feedback