Уровень 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-based | Operation-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 Analysis | CA1001: Dispose patterns |
| CS** | Compiler warnings/errors | CS8600: Nullability |
| IDE0 | Code Style | IDE0007: var keyword |
| IDE1 | Roslyn Analyzers | IDE0005: Unused using |
| RS | Roslyn SDK | RS1030: Analyzer constraints |
| SA** | StyleCop Analyzers | SA1101: Prefix local calls |
| VSTHRD | Microsoft.Threading | VSTHRD101: 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 SyntaxAction | Operation-based медленнее, но точнее |
SupportedDiagnostics | Кэшируйте ImmutableArray как static readonly |
Дизайн правил
| Практика | Описание |
| Уникальный ID | Диапазон 0000-0999 для кастомных правил |
| Severity | Warning для рекомендаций, 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<Func<TEntity, bool>> 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 Context | Context Diagram | Бизнес, архитекторы | Система + внешние actors |
| Containers | Container Diagram | Архитекторы, разработчики | Приложения, БД, очереди |
| Components | Component Diagram | Разработчики | Внутренние компоненты |
| Code | Class 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
| Principle | Description |
| 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
| Metric | Target | Rationale |
| Lines changed | < 400 LOC | Review quality drops significantly above this |
| Number of files | < 15 | Focus on related changes |
| Number of commits | < 10 | Logical grouping |
| Review time | < 4 hours (first review) | Fast feedback loop |
| Review cycles | 1-2 rounds | Indicates clear requirements |
| Time to merge | < 2 days | Avoid 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
- Self-review — просмотрите свой diff перед отправкой
- Small PRs — разбивайте большие изменения
- Clear description — объясните "что" и "почему"
- Tests included — тесты в том же PR
- Responsive — отвечайте на комментарии быстро
Для ревьюеров
- Read the description — поймите контекст перед кодом
- Review every line — не пропускайте файлы
- Focus on design — логика, архитектура, безопасность
- Be constructive — предлагайте решения, не только критикуйте
- Approve confidently — не блокируйте за личные предпочтения
Метрики team health
| Metric | Target | Why |
| Time to first review | < 4 hours | Fast feedback |
| Review cycles | 1-2 rounds | Clear requirements |
| PR size | < 400 LOC | Better review quality |
| Time to merge | < 2 days | Velocity |
| Review participation | Balanced | No 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/assembly | 100% безопасно |
| Change Signature | Изменение параметров метода | 100% безопасно |
| Encapsulate Field | Замена поля на property | 100% безопасно |
| 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 Type | Description | Example |
| 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
Ключевые метрики
| Metric | Key | Description | Target |
| Technical Debt | sqale_index | Суммарные усилия для исправления всех maintainability issues | < 5% of dev time |
| Technical Debt Ratio | sqale_debt_ratio | Соотношение debt / cost_to_develop | < 5% (A), < 10% (B) |
| Maintainability Rating | sqale_rating | A (0-5%), B (6-10%), C (11-20%), D (21-50%), E (51%+) | A или B |
| Code Smells | code_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
| Metric | Description | Tool |
| Churn Count | Количество коммитов, изменивших файл | git log |
| Churn Volume | Строки добавлены/удалены | git diff |
| Author Entropy | Shannon 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, &&, ||, ?:
Интерпретация
| Complexity | Rating | Action |
| 1-10 | Low | Идеально |
| 11-20 | Moderate | Приемлемо, но стоит проверить |
| 21-50 | High | Требуется рефакторинг |
| 51+ | Critical | Немедленный рефакторинг |
Когда высокая complexity оправдана
| Сценарий | Обоснование |
| State Machine | Состояния = decision points |
| Parser / Compiler | Grammar rules = branches |
| Rule Engine | Business 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 Score | Rating | Description |
| 100-25 | A | High maintainability |
| 24-10 | B | Moderate maintainability |
| 9--10 | C | Low maintainability |
| -11--50 | D | Very low maintainability |
| <-51 | E | Unmaintainable |
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