04Архитектура Приложений

Уровень 1: Foundation

Принципы проектирования

SOLID Principles

Single Responsibility Principle (SRP)

Определение: Класс должен иметь только одну причину для изменения. Одна ответственность = одна причина изменения.

Ключевое различие: SRP ≠ Separation of Concerns. SoC — это архитектурный принцип разделения системы на части с разными задачами. SRP — это принцип на уровне класса/модуля.

Плохой пример — God Class
// Нарушает SRP: отвечает за бизнес-логику, валидацию, логирование, отправку email, сохранение в БД
        public class OrderService
        {
            public void ProcessOrder(Order order)
            {
                // 1. Валидация
                if (order.Items.Count == 0) throw new ArgumentException("Order is empty");
                if (order.TotalAmount <= 0) throw new ArgumentException("Invalid amount");
        
                // 2. Бизнес-логика расчёта
                decimal tax = order.TotalAmount * 0.2m;
                decimal discount = order.TotalAmount > 1000 ? order.TotalAmount * 0.1m : 0;
                order.FinalAmount = order.TotalAmount + tax - discount;
        
                // 3. Логирование
                Console.WriteLine($"Processing order {order.Id} at {DateTime.UtcNow}");
        
                // 4. Сохранение в БД
                using var connection = new SqlConnection("...");
                connection.Execute("INSERT INTO Orders ...", order);
        
                // 5. Отправка email
                var client = new SmtpClient("smtp.example.com");
                client.Send("orders@example.com", order.CustomerEmail, "Order confirmed", "...");
        
                // 6. Обновление инвентаря
                foreach (var item in order.Items)
                {
                    connection.Execute("UPDATE Products SET Stock = Stock - @qty", new { item.ProductId, item.Quantity });
                }
            }
        }
Рефакторинг с применением SRP
// 1. Валидация — отдельный класс
        public interface IOrderValidator
        {
            ValidationResult Validate(Order order);
        }

        public class OrderValidator : IOrderValidator
        {
            public ValidationResult Validate(Order order)
            {
                if (order.Items.Count == 0) return ValidationResult.Fail("Order is empty");
                if (order.TotalAmount <= 0) return ValidationResult.Fail("Invalid amount");
                return ValidationResult.Success();
            }
        }

        // 2. Расчёт стоимости — отдельный класс
        public interface IOrderCalculator
        {
            decimal CalculateFinalAmount(Order order);
        }

        public class OrderCalculator : IOrderCalculator
        {
            private const decimal TaxRate = 0.2m;
            private const decimal DiscountThreshold = 1000m;
            private const decimal DiscountRate = 0.1m;

            public decimal CalculateFinalAmount(Order order)
            {
                var tax = order.TotalAmount * TaxRate;
                var discount = order.TotalAmount > DiscountThreshold ? order.TotalAmount * DiscountRate : 0;
                return order.TotalAmount + tax - discount;
            }
        }

        // 3. Репозиторий — отдельный класс
        public interface IOrderRepository
        {
            Task SaveAsync(Order order, CancellationToken ct = default);
            Task UpdateInventoryAsync(IEnumerable<OrderItem> items, CancellationToken ct = default);
        }

        // 4. Уведомления — отдельный класс
        public interface INotificationService
        {
            Task SendOrderConfirmationAsync(Order order, CancellationToken ct = default);
        }

        // 5. Координатор — тонкий класс, который только оркестрирует
        public class OrderProcessor
        {
            private readonly IOrderValidator _validator;
            private readonly IOrderCalculator _calculator;
            private readonly IOrderRepository _repository;
            private readonly INotificationService _notification;

            public OrderProcessor(
                IOrderValidator validator,
                IOrderCalculator calculator,
                IOrderRepository repository,
                INotificationService notification)
            {
                _validator = validator;
                _calculator = calculator;
                _repository = repository;
                _notification = notification;
            }

            public async Task ProcessOrderAsync(Order order, CancellationToken ct = default)
            {
                var validation = _validator.Validate(order);
                if (!validation.IsSuccess) throw new ValidationException(validation.Error);

                order.FinalAmount = _calculator.CalculateFinalAmount(order);
        
                await _repository.SaveAsync(order, ct);
                await _repository.UpdateInventoryAsync(order.Items, ct);
                await _notification.SendOrderConfirmationAsync(order, ct);
            }
        }

Преимущества:

  • Каждый класс можно тестировать изолированно
  • Легко заменить одну реализацию (например, email на SMS)
  • Изменения в логике расчёта не затрагивают валидацию
  • Параллельная разработка разными членами команды

Open/Closed Principle (OCP)

Определение: Сущности должны быть открыты для расширения, но закрыты для модификации.

Плохой пример — Switch/If-Else на стероидах
// Нарушает OCP: каждое новое условие требует модификации этого класса
        public class DiscountCalculator
        {
            public decimal CalculateDiscount(Order order, string customerType)
            {
                return customerType switch
                {
                    "Regular" => order.TotalAmount > 1000 ? order.TotalAmount * 0.05m : 0,
                    "Premium" => order.TotalAmount > 500 ? order.TotalAmount * 0.1m : order.TotalAmount * 0.05m,
                    "VIP" => order.TotalAmount * 0.15m,
                    "Wholesale" => order.TotalAmount * 0.2m,
                    _ => throw new ArgumentException($"Unknown customer type: {customerType}")
                };
            }
        }
Рефакторинг с OCP — Strategy Pattern
// Абстракция стратегии
        public interface IDiscountStrategy
        {
            decimal CalculateDiscount(Order order);
            string CustomerType { get; }
        }

        // Конкретные стратегии
        public class RegularDiscount : IDiscountStrategy
        {
            public string CustomerType => "Regular";
            public decimal CalculateDiscount(Order order) => 
                order.TotalAmount > 1000 ? order.TotalAmount * 0.05m : 0;
        }

        public class PremiumDiscount : IDiscountStrategy
        {
            public string CustomerType => "Premium";
            public decimal CalculateDiscount(Order order) => 
                order.TotalAmount > 500 ? order.TotalAmount * 0.1m : order.TotalAmount * 0.05m;
        }

        public class VIPDiscount : IDiscountStrategy
        {
            public string CustomerType => "VIP";
            public decimal CalculateDiscount(Order order) => order.TotalAmount * 0.15m;
        }

        // Контекст, который использует стратегии
        public class DiscountCalculator
        {
            private readonly IReadOnlyDictionary<string, IDiscountStrategy> _strategies;

            public DiscountCalculator(IEnumerable<IDiscountStrategy> strategies)
            {
                _strategies = strategies.ToDictionary(s => s.CustomerType);
            }

            public decimal CalculateDiscount(Order order, string customerType)
            {
                if (!_strategies.TryGetValue(customerType, out var strategy))
                    throw new ArgumentException($"Unknown customer type: {customerType}");
        
                return strategy.CalculateDiscount(order);
            }
        }

        // Регистрация в DI — добавление новой стратегии НЕ требует изменения DiscountCalculator
        builder.Services.AddSingleton<IDiscountStrategy, RegularDiscount>();
        builder.Services.AddSingleton<IDiscountStrategy, PremiumDiscount>();
        builder.Services.AddSingleton<IDiscountStrategy, VIPDiscount>();
        // Новая стратегия — просто добавляем ещё одну регистрацию
        builder.Services.AddSingleton<IDiscountStrategy, WholesaleDiscount>();

Liskov Substitution Principle (LSP)

Определение: Объекты базового типа должны быть заменяемы объектами производного типа без изменения корректности программы.

Формально: Если S — подтип T, то объекты типа T в программе могут быть заменены объектами типа S без изменения желательных свойств программы.

Нарушение LSP — классический пример с квадратом и прямоугольником
public class Rectangle
        {
            public virtual double Width { get; set; }
            public virtual double Height { get; set; }
    
            public double Area => Width * Height;
        }

        // Нарушает LSP: изменяет поведение унаследованных свойств
        public class Square : Rectangle
        {
            private double _side;
            public override double Width
            {
                get => _side;
                set => _side = value;
            }
            public override double Height
            {
                get => _side;
                set => _side = value;
            }
        }

        // Клиентский код ломается при подстановке Square
        public class AreaCalculator
        {
            public double CalculateArea(Rectangle rect)
            {
                rect.Width = 5;
                rect.Height = 10;
                return rect.Area; // Ожидаем 50, но для Square получим 100!
            }
        }
Правильное решение
// Общий интерфейс только для чтения
        public interface IShape
        {
            double Area { get; }
        }

        // Изменяемый прямоугольник
        public class Rectangle : IShape
        {
            public double Width { get; set; }
            public double Height { get; set; }
            public double Area => Width * Height;
        }

        // Квадрат — отдельная сущность
        public class Square : IShape
        {
            public double Side { get; set; }
            public double Area => Side * Side;
        }

        // Или: Value Objects без наследования
        public readonly record struct Rectangle(double Width, double Height)
        {
            public double Area => Width * Height;
        }

        public readonly record struct Square(double Side)
        {
            public double Area => Side * Side;
        }
Нарушение LSP в .NET — NotSupportedException
// Нарушает LSP: ReadOnlyCollection выбрасывает NotSupportedException
        // на методах Add/Remove, которые есть в базовом IList<T>
        var readOnly = new ReadOnlyCollection<int>(new List<int> { 1, 2, 3 });
        IList<int> list = readOnly;
        list.Add(4); // NotSupportedException!

Правило: Если производный класс выбрасывает NotSupportedException или NotImplementedException на методы базового — это почти всегда нарушение LSP.


Interface Segregation Principle (ISP)

Определение: Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше много маленьких интерфейсов, чем один большой.

Нарушение ISP — Fat Interface
// Нарушает ISP: принуждает реализовывать все методы
        public interface IRepository<T>
        {
            Task<T> GetByIdAsync(int id);
            Task<IEnumerable<T>> GetAllAsync();
            Task AddAsync(T entity);
            Task UpdateAsync(T entity);
            Task DeleteAsync(T entity);
            Task<int> CountAsync();
            Task<bool> ExistsAsync(int id);
            Task<IEnumerable<T>> SearchAsync(string query);
            Task ImportAsync(IEnumerable<T> entities);
            Task ExportAsync(Stream output);
        }

        // Read-only репозиторий вынужден реализовывать методы модификации
        public class ReadOnlyProductRepository : IRepository<Product>
        {
            public Task<Product> GetByIdAsync(int id) => ...
            public Task<IEnumerable<Product>> GetAllAsync() => ...
    
            // Не поддерживаемые операции
            public Task AddAsync(Product entity) => throw new NotSupportedException();
            public Task UpdateAsync(Product entity) => throw new NotSupportedException();
            public Task DeleteAsync(Product entity) => throw new NotSupportedException();
            public Task ImportAsync(IEnumerable<Product> entities) => throw new NotSupportedException();
            // ...
        }
Правильное решение — сегрегация интерфейсов
// Базовый интерфейс только для чтения
        public interface IReadableRepository<T>
        {
            Task<T> GetByIdAsync(int id);
            Task<IEnumerable<T>> GetAllAsync();
            Task<int> CountAsync();
            Task<bool> ExistsAsync(int id);
            Task<IEnumerable<T>> SearchAsync(string query);
        }

        // Интерфейс для записи
        public interface IWritableRepository<T>
        {
            Task AddAsync(T entity);
            Task UpdateAsync(T entity);
            Task DeleteAsync(T entity);
        }

        // Интерфейс для массовых операций
        public interface IBulkRepository<T>
        {
            Task ImportAsync(IEnumerable<T> entities);
            Task ExportAsync(Stream output);
        }

        // Полный репозиторий комбинирует интерфейсы
        public interface IRepository<T> : 
            IReadableRepository<T>, 
            IWritableRepository<T>

        // Read-only — только то, что нужно
        public interface IReadOnlyRepository<T> : IReadableRepository<T>

Dependency Inversion Principle (DIP)

Определение:

  1. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Нарушение DIP — прямая зависимость от конкретных классов
// Нарушает DIP: OrderService напрямую зависит от SqlOrderRepository и SmtpEmailSender
        public class OrderService
        {
            private readonly SqlOrderRepository _repository; // конкретная реализация
            private readonly SmtpEmailSender _emailSender;   // конкретная реализация

            public OrderService()
            {
                _repository = new SqlOrderRepository("connectionString");
                _emailSender = new SmtpEmailSender("smtp.example.com");
            }
    
            public async Task ProcessOrderAsync(Order order)
            {
                await _repository.SaveAsync(order);
                await _emailSender.SendAsync(order.CustomerEmail, "Order confirmed");
            }
        }
Правильное решение — зависимость от абстракций
// Абстракции (интерфейсы)
        public interface IOrderRepository
        {
            Task SaveAsync(Order order, CancellationToken ct = default);
        }

        public interface IEmailSender
        {
            Task SendAsync(string to, string subject, CancellationToken ct = default);
        }

        // Зависимость от абстракций через constructor injection
        public class OrderService
        {
            private readonly IOrderRepository _repository;
            private readonly IEmailSender _emailSender;

            public OrderService(IOrderRepository repository, IEmailSender emailSender)
            {
                _repository = repository;
                _emailSender = emailSender;
            }
    
            public async Task ProcessOrderAsync(Order order, CancellationToken ct = default)
            {
                await _repository.SaveAsync(order, ct);
                await _emailSender.SendAsync(order.CustomerEmail, "Order confirmed", ct);
            }
        }

        // Конкретные реализации зависят от абстракций
        public class SqlOrderRepository : IOrderRepository { ... }
        public class SmtpEmailSender : IEmailSender { ... }

        // Регистрация в DI
        builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
        builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();

DRY, KISS, YAGNI, Boy Scout Rule

DRY (Don't Repeat Yourself)

Принцип: Каждая часть знания должна иметь единственное, однозначное, авторитетное представление в системе.

Важно: DRY — не про удаление дублирования кода, а про устранение дублирования знания.

// Нарушение DRY: знание о формате даты дублируется
        public class OrderController
        {
            public string FormatDate(DateTime date) => date.ToString("dd.MM.yyyy HH:mm");
        }

        public class InvoiceController
        {
            public string FormatDate(DateTime date) => date.ToString("dd.MM.yyyy HH:mm");
        }

        // Решение: единый источник знания
        public static class DateTimeFormatters
        {
            public const string DefaultFormat = "dd.MM.yyyy HH:mm";
            public static string FormatDefault(DateTime date) => date.ToString(DefaultFormat);
        }

KISS (Keep It Simple, Stupid)

Принцип: Простота должна быть ключевой целью дизайна. Избегайте ненужной сложности.

// Избыточно сложно
        public class StringUtilities
        {
            public static string Transform(string input)
            {
                if (input == null) throw new ArgumentNullException();
                if (input.Length == 0) return input;
        
                var builder = new StringBuilder();
                for (int i = 0; i < input.Length; i++)
                {
                    builder.Append(char.ToUpper(input[i]));
                }
                return builder.ToString();
            }
        }

        // KISS
        public static class StringUtilities
        {
            public static string ToUpperSafe(string input) => input?.ToUpper() ?? string.Empty;
        }

YAGNI (You Ain't Gonna Need It)

Принцип: Не добавляйте функциональность, пока она не понадобится.

// YAGNI violation: абстракция для "будущих" баз данных, которые никогда не появятся
        public interface IDatabase
        {
            void Connect();
            void Disconnect();
            void ExecuteQuery(string sql);
            void ExecuteStoredProcedure(string name, params object[] parameters);
            void BeginTransaction();
            void CommitTransaction();
            void RollbackTransaction();
            // ... ещё 20 методов для "будущей поддержки" Oracle, MongoDB, etc.
        }

        // Реализуется только для SQL Server
        public class SqlServerDatabase : IDatabase { ... }

Boy Scout Rule

Принцип: Всегда оставляйте код чище, чем он был до вас.

  • Увидели дублирование? Вынесите в метод.
  • Увидели плохое имя? Переименуйте.
  • Увидели длинный метод? Разбейте.
  • Увидели магическое число? Создайте константу.

GRASP Patterns

Information Expert

Принцип: Назначайте ответственность тому классу, который обладает необходимой информацией.

// Плохо: контроллер делает вычисления, хотя информация в Order
        public class OrderController
        {
            public decimal CalculateTotal(Order order)
            {
                decimal total = 0;
                foreach (var item in order.Items)
                {
                    total += item.Price * item.Quantity;
                }
                return total;
            }
        }

        // Хорошо: Order знает свои items и может посчитать total
        public class Order
        {
            public IReadOnlyList<OrderItem> Items { get; }
    
            public decimal CalculateTotal() => 
                Items.Sum(item => item.Price * item.Quantity);
        }

Creator

Принцип: Назначайте классу B ответственность за создание экземпляра класса A, если:

  • B содержит или агрегирует A
  • B записывает A
  • B использует A
  • B имеет инициализирующие данные для A
// Order создаёт OrderItem — он содержит items и имеет данные для инициализации
        public class Order
        {
            private readonly List<OrderItem> _items = new();
            public IReadOnlyList<OrderItem> Items => _items;

            public void AddItem(Product product, int quantity)
            {
                var item = new OrderItem(product, quantity); // Creator
                _items.Add(item);
            }
        }

High Cohesion / Low Coupling

Cohesion (связность): Насколько тесно связаны методы класса с его основной ответственностью.

Coupling (зацепление): Насколько сильно класс зависит от других классов.

// Низкая связность, высокое зацепление
        public class UserService
        {
            public void RegisterUser(User user) { ... }
            public void SendEmail(string to, string body) { ... }
            public void GeneratePdfReport(ReportData data) { ... }
            public void ConnectToDatabase(string connectionString) { ... }
            public void LogToFile(string message) { ... }
        }
        // Зависит от: EmailService, PdfGenerator, Database, Logger — высокое зацепление

        // Высокая связность, низкое зацепление
        public class UserService
        {
            private readonly IUserRepository _repository;
            private readonly IEmailService _email;
    
            public UserService(IUserRepository repository, IEmailService email)
            {
                _repository = repository;
                _email = email;
            }
    
            public async Task RegisterUserAsync(User user)
            {
                await _repository.SaveAsync(user);
                await _email.SendWelcomeEmailAsync(user.Email);
            }
        }
        // Зависит только от абстракций, методы связаны одной задачей — управление пользователями

Composition over Inheritance

Принцип: Предпочитайте композицию (включение объектов) наследованию (is-a relationship).

Проблемы наследования:

  • Хрупкий базовый класс (Fragile Base Class)
  • Жёсткая связь на этапе компиляции
  • Невозможность изменения поведения в runtime
  • Нарушение инкапсуляции (базовый класс видит internals производного)
// Наследование — жёстко, сложно расширять
        public class Bird
        {
            public virtual void Fly() { }
            public virtual void Swim() { }
        }

        public class Penguin : Bird
        {
            public override void Fly() => throw new InvalidOperationException("Penguins can't fly!");
        }

        // Композиция — гибко, легко расширять
        public interface IFlyBehavior { void Fly(); }
        public interface ISwimBehavior { void Swim(); }

        public class NoFly : IFlyBehavior { public void Fly() { } }
        public class CanFly : IFlyBehavior { public void Fly() => Console.WriteLine("Flying"); }
        public class CanSwim : ISwimBehavior { public void Swim() => Console.WriteLine("Swimming"); }

        public class Bird
        {
            private readonly IFlyBehavior _flyBehavior;
            private readonly ISwimBehavior _swimBehavior;

            public Bird(IFlyBehavior fly, ISwimBehavior swim)
            {
                _flyBehavior = fly;
                _swimBehavior = swim;
            }

            public void Fly() => _flyBehavior.Fly();
            public void Swim() => _swimBehavior.Swim();
        }

        var penguin = new Bird(new NoFly(), new CanSwim());
        var eagle = new Bird(new CanFly(), new CanSwim());

Практика

Задание 1: Refactor God Class

Рефакторинг класса PaymentProcessor, который нарушает все принципы SOLID:

public class PaymentProcessor
        {
            public void ProcessPayment(Payment payment)
            {
                // Валидация + расчёт + логирование + БД + email + кэш + аналитика
                // ... 200+ строк кода
            }
        }

Задача: Разделить на классы с единственной ответственностью, применить DI, покрыть unit-тестами.

Задание 2: DI Container Lifecycle

Реализовать простой DI container с поддержкой:

  • Transient: новый экземпляр при каждом запросе
  • Scoped: один экземпляр на scope
  • Singleton: один экземпляр на всё время жизни

Задание 3: Unit Tests

Написать тесты, доказывающие:

  • Каждый класс после рефакторинга работает корректно
  • Стратегии взаимозаменяемы (LSP)
  • Новые стратегии добавляются без изменения существующего кода (OCP)

Паттерны проектирования (GoF + .NET-specific)

Creational Patterns

Singleton

Назначение: Гарантирует, что класс имеет только один экземпляр, и предоставляет глобальную точку доступа.

Thread-safe реализация в .NET
// Вариант 1: Lazy<T> — рекомендуемый подход
        public sealed class Configuration
        {
            private static readonly Lazy<Configuration> _instance = 
                new(() => new Configuration(), LazyThreadSafetyMode.ExecutionAndPublication);

            public static Configuration Instance => _instance.Value;

            private readonly Dictionary<string, string> _settings = new();

            private Configuration()
            {
                // Загрузка настроек из файла, БД, etc.
                _settings["MaxRetries"] = "3";
                _settings["Timeout"] = "30";
            }

            public string GetSetting(string key) => 
                _settings.TryGetValue(key, out var value) ? value : null;
        }

        // Вариант 2: Double-check locking
        public sealed class DatabaseConnectionPool
        {
            private static DatabaseConnectionPool _instance;
            private static readonly object _lock = new();

            public static DatabaseConnectionPool Instance
            {
                get
                {
                    if (_instance == null)
                    {
                        lock (_lock)
                        {
                            if (_instance == null)
                            {
                                _instance = new DatabaseConnectionPool();
                            }
                        }
                    }
                    return _instance;
                }
            }

            private DatabaseConnectionPool() { }
        }

Важно: В .NET DI контейнере Singleton регистрируется через AddSingleton<T>() — это предпочтительнее ручного паттерна.


Factory Method

Назначение: Определяет интерфейс для создания объекта, но позволяет подклассам решить, какой класс инстанцировать.

// Продукт
        public interface IPaymentGateway
        {
            Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request, CancellationToken ct = default);
        }

        // Конкретные продукты
        public class StripeGateway : IPaymentGateway { ... }
        public class PayPalGateway : IPaymentGateway { ... }
        public class SquareGateway : IPaymentGateway { ... }

        // Creator с Factory Method
        public abstract class PaymentGatewayFactory
        {
            public abstract IPaymentGateway CreateGateway();

            public IPaymentGateway GetConfiguredGateway()
            {
                var gateway = CreateGateway();
                // Общая логика настройки для всех gateway
                ConfigureGateway(gateway);
                return gateway;
            }

            protected virtual void ConfigureGateway(IPaymentGateway gateway) { }
        }

        // Concrete Creators
        public class StripeFactory : PaymentGatewayFactory
        {
            private readonly string _apiKey;
            public StripeFactory(string apiKey) => _apiKey = apiKey;
    
            public override IPaymentGateway CreateGateway() => new StripeGateway(_apiKey);
        }

        public class PayPalFactory : PaymentGatewayFactory
        {
            private readonly string _clientId;
            private readonly string _clientSecret;
    
            public PayPalFactory(string clientId, string clientSecret)
            {
                _clientId = clientId;
                _clientSecret = clientSecret;
            }
    
            public override IPaymentGateway CreateGateway() => new PayPalGateway(_clientId, _clientSecret);
        }

Abstract Factory

Назначение: Предоставляет интерфейс для создания семейств связанных объектов без указания их конкретных классов.

// Семейство продуктов — UI элементы
        public interface IButton { void Render(); }
        public interface ICheckbox { void Render(); }
        public interface ITextBox { void Render(); }

        // Factory interface
        public interface IUIFactory
        {
            IButton CreateButton();
            ICheckbox CreateCheckbox();
            ITextBox CreateTextBox();
        }

        // Concrete factories — Windows
        public class WindowsUIFactory : IUIFactory
        {
            public IButton CreateButton() => new WindowsButton();
            public ICheckbox CreateCheckbox() => new WindowsCheckbox();
            public ITextBox CreateTextBox() => new WindowsTextBox();
        }

        // Concrete factories — Web
        public class WebUIFactory : IUIFactory
        {
            public IButton CreateButton() => new HtmlButton();
            public ICheckbox CreateCheckbox() => new HtmlCheckbox();
            public ITextBox CreateTextBox() => new HtmlTextBox();
        }

        // Client — работает с абстрактной фабрикой
        public class Dialog
        {
            private readonly IUIFactory _factory;

            public Dialog(IUIFactory factory) => _factory = factory;

            public void Render()
            {
                _factory.CreateButton().Render();
                _factory.CreateCheckbox().Render();
                _factory.CreateTextBox().Render();
            }
        }

Builder

Назначение: Отделяет конструирование сложного объекта от его представления.

// Продукт
        public class HttpRequest
        {
            public string Method { get; init; }
            public Uri Uri { get; init; }
            public Dictionary<string, string> Headers { get; } = new();
            public HttpContent Content { get; init; }
            public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
            public bool FollowRedirects { get; init; } = true;
        }

        // Builder
        public class HttpRequestBuilder
        {
            private string _method = "GET";
            private Uri _uri;
            private readonly Dictionary<string, string> _headers = new();
            private HttpContent _content;
            private TimeSpan _timeout = TimeSpan.FromSeconds(30);
            private bool _followRedirects = true;

            public HttpRequestBuilder WithMethod(string method)
            {
                _method = method;
                return this;
            }

            public HttpRequestBuilder WithUri(string uri)
            {
                _uri = new Uri(uri);
                return this;
            }

            public HttpRequestBuilder WithHeader(string name, string value)
            {
                _headers[name] = value;
                return this;
            }

            public HttpRequestBuilder WithContent(HttpContent content)
            {
                _content = content;
                return this;
            }

            public HttpRequestBuilder WithTimeout(TimeSpan timeout)
            {
                _timeout = timeout;
                return this;
            }

            public HttpRequestBuilder NoRedirects()
            {
                _followRedirects = false;
                return this;
            }

            public HttpRequest Build()
            {
                if (_uri == null) throw new InvalidOperationException("Uri is required");
        
                var request = new HttpRequest
                {
                    Method = _method,
                    Uri = _uri,
                    Content = _content,
                    Timeout = _timeout,
                    FollowRedirects = _followRedirects
                };
        
                foreach (var (key, value) in _headers)
                    request.Headers[key] = value;

                return request;
            }
        }

        // Fluent API использование
        var request = new HttpRequestBuilder()
            .WithMethod("POST")
            .WithUri("https://api.example.com/orders")
            .WithHeader("Authorization", "Bearer token")
            .WithHeader("Content-Type", "application/json")
            .WithContent(new StringContent("{\"item\": 1}"))
            .WithTimeout(TimeSpan.FromSeconds(60))
            .Build();

Prototype

Назначение: Создание новых объектов путём копирования существующего прототипа.

public abstract class Document : ICloneable
        {
            public string Title { get; set; }
            public string Author { get; set; }
            public List<string> Tags { get; set; } = new();
    
            public abstract Document Clone();
        }

        public class Report : Document
        {
            public string ReportType { get; set; }
            public Dictionary<string, object> Data { get; set; } = new();

            public override Document Clone()
            {
                // Deep clone
                return new Report
                {
                    Title = this.Title,
                    Author = this.Author,
                    Tags = new List<string>(this.Tags),
                    ReportType = this.ReportType,
                    Data = new Dictionary<string, object>(this.Data)
                };
            }
        }

        // Использование
        var template = new Report
        {
            Title = "Monthly Report",
            Author = "System",
            Tags = new List<string> { "monthly", "auto" },
            ReportType = "Financial"
        };

        var januaryReport = (Report)template.Clone();
        januaryReport.Title = "January Report";
        januaryReport.Data["Month"] = "January";

        var februaryReport = (Report)template.Clone();
        februaryReport.Title = "February Report";
        februaryReport.Data["Month"] = "February";

Object Pool

Назначение: Переиспользование дорогостоящих объектов вместо создания новых.

public class ExpensiveResource
        {
            public int Id { get; }
            public DateTime CreatedAt { get; }
            public bool IsInUse { get; set; }

            public ExpensiveResource(int id)
            {
                Id = id;
                CreatedAt = DateTime.UtcNow;
                Console.WriteLine($"Resource {id} created");
            }

            public void Reset()
            {
                IsInUse = false;
                Console.WriteLine($"Resource {id} reset");
            }
        }

        public class ObjectPool<T> where T : class
        {
            private readonly ConcurrentBag<T> _available = new();
            private readonly Func<T> _factory;
            private readonly Action<T> _resetAction;
            private int _totalCreated;

            public ObjectPool(Func<T> factory, Action<T> resetAction, int initialSize = 0)
            {
                _factory = factory;
                _resetAction = resetAction;
        
                for (int i = 0; i < initialSize; i++)
                {
                    _available.Add(CreateNew());
                }
            }

            public T Get()
            {
                if (_available.TryTake(out var item))
                {
                    return item;
                }
                return CreateNew();
            }

            public void Return(T item)
            {
                _resetAction(item);
                _available.Add(item);
            }

            private T CreateNew()
            {
                Interlocked.Increment(ref _totalCreated);
                return _factory();
            }
        }

        // Использование
        var pool = new ObjectPool<ExpensiveResource>(
            () => new ExpensiveResource(Interlocked.Increment(ref _counter)),
            r => r.Reset(),
            initialSize: 5
        );

        var resource = pool.Get();
        try
        {
            // Используем resource
        }
        finally
        {
            pool.Return(resource);
        }

Structural Patterns

Adapter

Назначение: Преобразует интерфейс одного класса в интерфейс, ожидаемый клиентом.

// Существующий legacy сервис
        public class LegacyPaymentProcessor
        {
            public bool MakePayment(string cardNumber, decimal amount, string currency)
            {
                // Legacy API
                return true;
            }
        }

        // Целевой интерфейс
        public interface IPaymentService
        {
            Task<PaymentResult> ProcessAsync(PaymentRequest request, CancellationToken ct = default);
        }

        // Адаптер
        public class LegacyPaymentAdapter : IPaymentService
        {
            private readonly LegacyPaymentProcessor _legacy;

            public LegacyPaymentAdapter(LegacyPaymentProcessor legacy) => _legacy = legacy;

            public Task<PaymentResult> ProcessAsync(PaymentRequest request, CancellationToken ct = default)
            {
                bool success = _legacy.MakePayment(
                    request.CardNumber,
                    request.Amount,
                    request.Currency
                );

                return Task.FromResult(new PaymentResult
                {
                    Success = success,
                    TransactionId = Guid.NewGuid().ToString(),
                    Amount = request.Amount
                });
            }
        }

Decorator

Назначение: Динамически добавляет объекту новую функциональность, оборачивая его.

// Базовый интерфейс
        public interface IEmailService
        {
            Task SendAsync(string to, string subject, string body, CancellationToken ct = default);
        }

        // Базовая реализация
        public class SmtpEmailService : IEmailService
        {
            public async Task SendAsync(string to, string subject, string body, CancellationToken ct = default)
            {
                // Отправка через SMTP
                await Task.Delay(100, ct);
                Console.WriteLine($"Email sent to {to}");
            }
        }

        // Decorator: логирование
        public class LoggingEmailDecorator : IEmailService
        {
            private readonly IEmailService _inner;
            private readonly ILogger<LoggingEmailDecorator> _logger;

            public LoggingEmailDecorator(IEmailService inner, ILogger<LoggingEmailDecorator> logger)
            {
                _inner = inner;
                _logger = logger;
            }

            public async Task SendAsync(string to, string subject, string body, CancellationToken ct = default)
            {
                _logger.LogInformation("Sending email to {To} with subject {Subject}", to, subject);
                try
                {
                    await _inner.SendAsync(to, subject, body, ct);
                    _logger.LogInformation("Email sent successfully to {To}", to);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Failed to send email to {To}", to);
                    throw;
                }
            }
        }

        // Decorator: retry
        public class RetryEmailDecorator : IEmailService
        {
            private readonly IEmailService _inner;
            private readonly int _maxRetries;

            public RetryEmailDecorator(IEmailService inner, int maxRetries = 3)
            {
                _inner = inner;
                _maxRetries = maxRetries;
            }

            public async Task SendAsync(string to, string subject, string body, CancellationToken ct = default)
            {
                for (int i = 0; i < _maxRetries; i++)
                {
                    try
                    {
                        await _inner.SendAsync(to, subject, body, ct);
                        return;
                    }
                    catch when (i < _maxRetries - 1)
                    {
                        await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)), ct);
                    }
                }
            }
        }

        // Decorator: queue (асинхронная отправка)
        public class QueuedEmailDecorator : IEmailService
        {
            private readonly IEmailService _inner;
            private readonly Channel<EmailMessage> _queue;

            public QueuedEmailDecorator(IEmailService inner)
            {
                _inner = inner;
                _queue = Channel.CreateBounded<EmailMessage>(1000);
                _ = ProcessQueueAsync();
            }

            public Task SendAsync(string to, string subject, string body, CancellationToken ct = default)
            {
                var message = new EmailMessage(to, subject, body);
                return _queue.Writer.WriteAsync(message, ct).AsTask();
            }

            private async Task ProcessQueueAsync()
            {
                await foreach (var message in _queue.Reader.ReadAllAsync())
                {
                    await _inner.SendAsync(message.To, message.Subject, message.Body);
                }
            }
        }

        // Регистрация цепочки декораторов в DI
        builder.Services.AddSingleton<IEmailService, SmtpEmailService>();
        builder.Services.Decorate<IEmailService, LoggingEmailDecorator>();
        builder.Services.Decorate<IEmailService, RetryEmailDecorator>();
        // Scrutor package для Decorate

Facade

Назначение: Предоставляет унифицированный интерфейс к набору интерфейсов в подсистеме.

// Подсистема: множество сложных классов
        public class InventoryService { public bool CheckStock(int productId) => true; }
        public class PricingService { public decimal GetPrice(int productId) => 100m; }
        public class ShippingService { public decimal CalculateShipping(Address address) => 10m; }
        public class TaxService { public decimal CalculateTax(decimal amount) => amount * 0.2m; }
        public class NotificationService { public Task SendConfirmationAsync(string email) => Task.CompletedTask; }

        // Facade — простой интерфейс для клиента
        public class OrderFacade
        {
            private readonly InventoryService _inventory;
            private readonly PricingService _pricing;
            private readonly ShippingService _shipping;
            private readonly TaxService _tax;
            private readonly NotificationService _notification;

            public OrderFacade(
                InventoryService inventory,
                PricingService pricing,
                ShippingService shipping,
                TaxService tax,
                NotificationService notification)
            {
                _inventory = inventory;
                _pricing = pricing;
                _shipping = shipping;
                _tax = tax;
                _notification = notification;
            }

            public async Task<OrderResult> PlaceOrderAsync(OrderRequest request, CancellationToken ct = default)
            {
                if (!_inventory.CheckStock(request.ProductId))
                    throw new InvalidOperationException("Product out of stock");

                var price = _pricing.GetPrice(request.ProductId);
                var shipping = _shipping.CalculateShipping(request.Address);
                var tax = _tax.CalculateTax(price);
                var total = price + shipping + tax;

                await _notification.SendConfirmationAsync(request.CustomerEmail);

                return new OrderResult
                {
                    ProductId = request.ProductId,
                    TotalAmount = total,
                    Tax = tax,
                    Shipping = shipping
                };
            }
        }

Proxy

Назначение: Предоставляет суррогат или заглушку для другого объекта для контроля доступа.

// Реальный сервис
        public interface IImageService
        {
            byte[] LoadImage(string url);
        }

        public class RemoteImageService : IImageService
        {
            public byte[] LoadImage(string url)
            {
                // Дорогостоящая операция — загрузка из сети
                using var client = new HttpClient();
                return client.GetByteArrayAsync(url).Result;
            }
        }

        // Virtual Proxy — ленивая загрузка
        public class LazyImageProxy : IImageService
        {
            private readonly string _url;
            private IImageService _realService;
            private byte[] _cachedImage;

            public LazyImageProxy(string url) => _url = url;

            public byte[] LoadImage(string url)
            {
                if (_cachedImage == null)
                {
                    _realService ??= new RemoteImageService();
                    _cachedImage = _realService.LoadImage(_url);
                }
                return _cachedImage;
            }
        }

        // Protection Proxy — контроль доступа
        public class ProtectedImageService : IImageService
        {
            private readonly IImageService _inner;
            private readonly IAuthorizationService _auth;

            public ProtectedImageService(IImageService inner, IAuthorizationService auth)
            {
                _inner = inner;
                _auth = auth;
            }

            public byte[] LoadImage(string url)
            {
                if (!_auth.IsAuthorized("ImageAccess"))
                    throw new UnauthorizedAccessException("Access denied");
        
                return _inner.LoadImage(url);
            }
        }

Behavioral Patterns

Strategy

Назначение: Определяет семейство алгоритмов, инкапсулирует каждый и делает их взаимозаменяемыми.

// Стратегия
        public interface ICompressionStrategy
        {
            byte[] Compress(byte[] data);
            byte[] Decompress(byte[] data);
            string AlgorithmName { get; }
        }

        // Конкретные стратегии
        public class GzipCompression : ICompressionStrategy
        {
            public string AlgorithmName => "GZIP";
    
            public byte[] Compress(byte[] data)
            {
                using var output = new MemoryStream();
                using var gzip = new GZipStream(output, CompressionLevel.Optimal);
                gzip.Write(data, 0, data.Length);
                return output.ToArray();
            }

            public byte[] Decompress(byte[] data)
            {
                using var input = new MemoryStream(data);
                using var gzip = new GZipStream(input, CompressionMode.Decompress);
                using var output = new MemoryStream();
                gzip.CopyTo(output);
                return output.ToArray();
            }
        }

        public class BrotliCompression : ICompressionStrategy
        {
            public string AlgorithmName => "BROTLI";
    
            public byte[] Compress(byte[] data)
            {
                using var output = new MemoryStream();
                using var brotli = new BrotliStream(output, CompressionLevel.Optimal);
                brotli.Write(data, 0, data.Length);
                return output.ToArray();
            }

            public byte[] Decompress(byte[] data)
            {
                using var input = new MemoryStream(data);
                using var brotli = new BrotliStream(input, CompressionMode.Decompress);
                using var output = new MemoryStream();
                brotli.CopyTo(output);
                return output.ToArray();
            }
        }

        // Контекст
        public class CompressionService
        {
            private readonly ICompressionStrategy _strategy;

            public CompressionService(ICompressionStrategy strategy) => _strategy = strategy;

            public byte[] Compress(byte[] data) => _strategy.Compress(data);
            public byte[] Decompress(byte[] data) => _strategy.Decompress(data);
            public string Algorithm => _strategy.AlgorithmName;
        }

Observer

Назначение: Определяет зависимость "один ко многим" между объектами так, что при изменении состояния одного все зависимые уведомляются.

// .NET встроенный подход — events
        public class StockPrice
        {
            public string Symbol { get; }
            public decimal Price { get; private set; }

            public event EventHandler<PriceChangedEventArgs> PriceChanged;

            public StockPrice(string symbol) => Symbol = symbol;

            public void UpdatePrice(decimal newPrice)
            {
                if (Price != newPrice)
                {
                    var oldPrice = Price;
                    Price = newPrice;
                    PriceChanged?.Invoke(this, new PriceChangedEventArgs(Symbol, oldPrice, newPrice));
                }
            }
        }

        public class PriceChangedEventArgs : EventArgs
        {
            public string Symbol { get; }
            public decimal OldPrice { get; }
            public decimal NewPrice { get; }

            public PriceChangedEventArgs(string symbol, decimal oldPrice, decimal newPrice)
            {
                Symbol = symbol;
                OldPrice = oldPrice;
                NewPrice = newPrice;
            }
        }

        // Подписчики
        public class PriceAlertObserver
        {
            private readonly decimal _threshold;

            public PriceAlertObserver(decimal threshold) => _threshold = threshold;

            public void OnPriceChanged(object sender, PriceChangedEventArgs e)
            {
                if (e.NewPrice >= _threshold)
                {
                    Console.WriteLine($"ALERT: {e.Symbol} reached {e.NewPrice} (threshold: {_threshold})");
                }
            }
        }

        public class LoggerObserver
        {
            public void OnPriceChanged(object sender, PriceChangedEventArgs e)
            {
                Console.WriteLine($"[{DateTime.UtcNow:HH:mm:ss}] {e.Symbol}: {e.OldPrice} -> {e.NewPrice}");
            }
        }

        // Использование
        var stock = new StockPrice("AAPL");
        stock.PriceChanged += new PriceAlertObserver(150).OnPriceChanged;
        stock.PriceChanged += new LoggerObserver().OnPriceChanged;

        stock.UpdatePrice(145);
        stock.UpdatePrice(152); // Trigger alert

Command

Назначение: Инкапсулирует запрос как объект, позволяя параметризовать клиентов с разными запросами.

// Команда
        public interface ICommand
        {
            void Execute();
            void Undo();
        }

        // Конкретные команды
        public class CreateOrderCommand : ICommand
        {
            private readonly IOrderRepository _repository;
            private readonly Order _order;

            public CreateOrderCommand(IOrderRepository repository, Order order)
            {
                _repository = repository;
                _order = order;
            }

            public void Execute() => _repository.Add(_order);
            public void Undo() => _repository.Delete(_order.Id);
        }

        public class UpdateOrderStatusCommand : ICommand
        {
            private readonly IOrderRepository _repository;
            private readonly int _orderId;
            private readonly OrderStatus _newStatus;
            private OrderStatus _oldStatus;

            public UpdateOrderStatusCommand(IOrderRepository repository, int orderId, OrderStatus newStatus)
            {
                _repository = repository;
                _orderId = orderId;
                _newStatus = newStatus;
            }

            public void Execute()
            {
                var order = _repository.GetById(_orderId);
                _oldStatus = order.Status;
                order.Status = _newStatus;
                _repository.Update(order);
            }

            public void Undo()
            {
                var order = _repository.GetById(_orderId);
                order.Status = _oldStatus;
                _repository.Update(order);
            }
        }

        // Invoker с undo/redo
        public class CommandHistory
        {
            private readonly Stack<ICommand> _undoStack = new();
            private readonly Stack<ICommand> _redoStack = new();

            public void Execute(ICommand command)
            {
                command.Execute();
                _undoStack.Push(command);
                _redoStack.Clear();
            }

            public bool CanUndo => _undoStack.Count > 0;
            public bool CanRedo => _redoStack.Count > 0;

            public void Undo()
            {
                if (!CanUndo) return;
                var command = _undoStack.Pop();
                command.Undo();
                _redoStack.Push(command);
            }

            public void Redo()
            {
                if (!CanRedo) return;
                var command = _redoStack.Pop();
                command.Execute();
                _undoStack.Push(command);
            }
        }

Chain of Responsibility

Назначение: Позволяет передавать запрос по цепочке обработчиков.

// Обработчик
        public abstract class Handler
        {
            protected Handler _next;

            public Handler SetNext(Handler handler)
            {
                _next = handler;
                return handler;
            }

            public abstract Task<ValidationResult> HandleAsync(Request request, CancellationToken ct = default);
        }

        // Конкретные обработчики
        public class AuthenticationHandler : Handler
        {
            public override async Task<ValidationResult> HandleAsync(Request request, CancellationToken ct = default)
            {
                if (string.IsNullOrEmpty(request.Token))
                    return ValidationResult.Fail("Authentication required");

                if (!IsValidToken(request.Token))
                    return ValidationResult.Fail("Invalid token");

                return _next != null ? await _next.HandleAsync(request, ct) : ValidationResult.Success();
            }

            private bool IsValidToken(string token) => token.StartsWith("Bearer ");
        }

        public class AuthorizationHandler : Handler
        {
            private readonly HashSet<string> _adminEndpoints = new() { "/admin", "/settings" };

            public override async Task<ValidationResult> HandleAsync(Request request, CancellationToken ct = default)
            {
                if (_adminEndpoints.Contains(request.Path) && !request.Roles.Contains("Admin"))
                    return ValidationResult.Fail("Admin access required");

                return _next != null ? await _next.HandleAsync(request, ct) : ValidationResult.Success();
            }
        }

        public class RateLimitHandler : Handler
        {
            private readonly Dictionary<string, Queue<DateTime>> _requests = new();
            private readonly int _maxRequestsPerMinute = 60;

            public override async Task<ValidationResult> HandleAsync(Request request, CancellationToken ct = default)
            {
                var now = DateTime.UtcNow;
                if (!_requests.TryGetValue(request.ClientId, out var timestamps))
                {
                    timestamps = new Queue<DateTime>();
                    _requests[request.ClientId] = timestamps;
                }

                // Очистка старых запросов
                while (timestamps.Count > 0 && timestamps.Peek() < now.AddMinutes(-1))
                    timestamps.Dequeue();

                if (timestamps.Count >= _maxRequestsPerMinute)
                    return ValidationResult.Fail("Rate limit exceeded");

                timestamps.Enqueue(now);
                return _next != null ? await _next.HandleAsync(request, ct) : ValidationResult.Success();
            }
        }

        // Построение цепочки
        var chain = new AuthenticationHandler()
            .SetNext(new AuthorizationHandler())
            .SetNext(new RateLimitHandler());

        var result = await chain.HandleAsync(request);

Mediator

Назначение: Определяет объект, инкапсулирующий то, как взаимодействует набор объектов.

// Mediator интерфейс
        public interface IChatMediator
        {
            void SendMessage(string message, User sender);
            void AddUser(User user);
        }

        // Concrete Mediator
        public class ChatRoom : IChatMediator
        {
            private readonly List<User> _users = new();

            public void AddUser(User user)
            {
                _users.Add(user);
                user.SetMediator(this);
            }

            public void SendMessage(string message, User sender)
            {
                foreach (var user in _users.Where(u => u != sender))
                {
                    user.ReceiveMessage(message, sender.Name);
                }
            }
        }

        // Colleague
        public abstract class User
        {
            public string Name { get; }
            protected IChatMediator _mediator;

            protected User(string name) => Name = name;

            public void SetMediator(IChatMediator mediator) => _mediator = mediator;

            public abstract void SendMessage(string message);
            public abstract void ReceiveMessage(string message, string from);
        }

        public class ConcreteUser : User
        {
            public ConcreteUser(string name) : base(name) { }

            public override void SendMessage(string message) => _mediator.SendMessage(message, this);
    
            public override void ReceiveMessage(string message, string from) =>
                Console.WriteLine($"[{Name}] received from {from}: {message}");
        }

        // Использование
        var chatRoom = new ChatRoom();
        var alice = new ConcreteUser("Alice");
        var bob = new ConcreteUser("Bob");
        var charlie = new ConcreteUser("Charlie");

        chatRoom.AddUser(alice);
        chatRoom.AddUser(bob);
        chatRoom.AddUser(charlie);

        alice.SendMessage("Hello everyone!");

Specification

Назначение: Инкапсулирует бизнес-правило, которое можно переиспользовать и комбинировать.

// Базовая спецификация
        public interface ISpecification<T>
        {
            bool IsSatisfiedBy(T entity);
            Expression<Func<T, bool>> ToExpression();
        }

        public abstract class BaseSpecification<T> : ISpecification<T>
        {
            public abstract Expression<Func<T, bool>> ToExpression();

            public bool IsSatisfiedBy(T entity) => ToExpression().Compile()(entity);
        }

        // Конкретные спецификации
        public class OrderTotalSpecification : BaseSpecification<Order>
        {
            private readonly decimal _minAmount;
            private readonly decimal _maxAmount;

            public OrderTotalSpecification(decimal minAmount, decimal maxAmount)
            {
                _minAmount = minAmount;
                _maxAmount = maxAmount;
            }

            public override Expression<Func<Order, bool>> ToExpression() =>
                o => o.TotalAmount >= _minAmount && o.TotalAmount <= _maxAmount;
        }

        public class ActiveCustomerSpecification : BaseSpecification<Order>
        {
            public override Expression<Func<Order, bool>> ToExpression() =>
                o => o.Customer.IsActive && !o.Customer.IsBanned;
        }

        // Комбинирование спецификаций
        public static class SpecificationExtensions
        {
            public static ISpecification<T> And<T>(this ISpecification<T> left, ISpecification<T> right) =>
                new AndSpecification<T>(left, right);

            public static ISpecification<T> Or<T>(this ISpecification<T> left, ISpecification<T> right) =>
                new OrSpecification<T>(left, right);

            public static ISpecification<T> Not<T>(this ISpecification<T> spec) =>
                new NotSpecification<T>(spec);
        }

        public class AndSpecification<T> : BaseSpecification<T>
        {
            private readonly ISpecification<T> _left;
            private readonly ISpecification<T> _right;

            public AndSpecification(ISpecification<T> left, ISpecification<T> right)
            {
                _left = left;
                _right = right;
            }

            public override Expression<Func<T, bool>> ToExpression()
            {
                var leftExpr = _left.ToExpression();
                var rightExpr = _right.ToExpression();
        
                var parameter = leftExpr.Parameters[0];
                var body = Expression.AndAlso(leftExpr.Body, 
                    new ParameterReplacer(rightExpr.Parameters[0], parameter).Visit(rightExpr.Body));
        
                return Expression.Lambda<Func<T, bool>>(body, parameter);
            }
        }

        // Использование
        var spec = new OrderTotalSpecification(100, 10000)
            .And(new ActiveCustomerSpecification());

        var orders = dbContext.Orders.Where(spec.ToExpression()).ToList();

.NET-specific Patterns

Repository

// Generic repository base
        public interface IRepository<TEntity, TKey> where TEntity : class
        {
            Task<TEntity> GetByIdAsync(TKey id, CancellationToken ct = default);
            Task<IReadOnlyList<TEntity>> GetAllAsync(CancellationToken ct = default);
            Task AddAsync(TEntity entity, CancellationToken ct = default);
            Task UpdateAsync(TEntity entity, CancellationToken ct = default);
            Task DeleteAsync(TKey id, CancellationToken ct = default);
        }

        public abstract class EfRepository<TEntity, TKey> : IRepository<TEntity, TKey> 
            where TEntity : class
        {
            protected readonly DbContext _context;
            protected readonly DbSet<TEntity> _dbSet;

            protected EfRepository(DbContext context)
            {
                _context = context;
                _dbSet = context.Set<TEntity>();
            }

            public virtual async Task<TEntity> GetByIdAsync(TKey id, CancellationToken ct = default) =>
                await _dbSet.FindAsync(new object[] { id }, cancellationToken: ct);

            public virtual async Task<IReadOnlyList<TEntity>> GetAllAsync(CancellationToken ct = default) =>
                await _dbSet.ToListAsync(ct);

            public virtual async Task AddAsync(TEntity entity, CancellationToken ct = default)
            {
                await _dbSet.AddAsync(entity, ct);
            }

            public virtual Task UpdateAsync(TEntity entity, CancellationToken ct = default)
            {
                _dbSet.Update(entity);
                return Task.CompletedTask;
            }

            public virtual async Task DeleteAsync(TKey id, CancellationToken ct = default)
            {
                var entity = await GetByIdAsync(id, ct);
                if (entity != null) _dbSet.Remove(entity);
            }
        }

Unit of Work

public interface IUnitOfWork : IDisposable
        {
            IOrderRepository Orders { get; }
            IProductRepository Products { get; }
            Task<int> SaveChangesAsync(CancellationToken ct = default);
            Task BeginTransactionAsync(CancellationToken ct = default);
            Task CommitTransactionAsync(CancellationToken ct = default);
            Task RollbackTransactionAsync(CancellationToken ct = default);
        }

        public class EfUnitOfWork : IUnitOfWork
        {
            private readonly AppDbContext _context;
            private IDbContextTransaction _transaction;

            public IOrderRepository Orders { get; }
            public IProductRepository Products { get; }

            public EfUnitOfWork(AppDbContext context)
            {
                _context = context;
                Orders = new EfOrderRepository(context);
                Products = new EfProductRepository(context);
            }

            public async Task<int> SaveChangesAsync(CancellationToken ct = default) =>
                await _context.SaveChangesAsync(ct);

            public async Task BeginTransactionAsync(CancellationToken ct = default) =>
                _transaction = await _context.Database.BeginTransactionAsync(ct);

            public async Task CommitTransactionAsync(CancellationToken ct = default)
            {
                await _transaction.CommitAsync(ct);
                await _transaction.DisposeAsync();
                _transaction = null;
            }

            public async Task RollbackTransactionAsync(CancellationToken ct = default)
            {
                await _transaction.RollbackAsync(ct);
                await _transaction.DisposeAsync();
                _transaction = null;
            }

            public void Dispose()
            {
                _transaction?.Dispose();
                _context.Dispose();
            }
        }

Практика

Задание 1: Strategy для Payment Processing

Реализовать систему обработки платежей с разными провайдерами (Stripe, PayPal, Crypto). Каждый провайдер — отдельная стратегия. Добавить фабрику для выбора стратегии на основе типа платежа.

Задание 2: Decorator Chain для HTTP Middleware

Создать цепочку декораторов для HTTP клиента: Logging → Retry → Circuit Breaker → Caching → Base HttpClient.

Задание 3: Command с Undo/Redo

Реализовать текстовый редактор с командами: InsertText, DeleteText, FormatText. Каждая команда поддерживает Undo/Redo через CommandHistory.


Dependency Injection в .NET

IServiceCollection и IServiceProvider — Internals

Как работает DI Container в .NET

.NET built-in DI — это conforming container минимальной функциональности. Он не является полноценным IoC контейнером как Autofac или DryIoc, но покрывает 95% use cases.

IServiceCollection (регистрация)
               ↓
           BuildServiceProvider()
               ↓
        IServiceProvider (резолвинг)
               ↓
           GetService<T>() / GetRequiredService<T>()
               ↓
           Создание графа зависимостей

ServiceDescriptor — структура регистрации

// Каждая регистрация — это ServiceDescriptor
        public class ServiceDescriptor
        {
            public Type ServiceType { get; }      // Интерфейс/абстракция
            public Type? ImplementationType { get; }  // Конкретная реализация
            public object? ImplementationInstance { get; }  // Готовый экземпляр (singleton)
            public Func<IServiceProvider, object>? ImplementationFactory { get; }  // Factory
            public ServiceLifetime Lifetime { get; }  // Transient/Scoped/Singleton
        }

        // Примеры создания ServiceDescriptor
        var descriptor1 = ServiceDescriptor.Transient<IEmailSender, SmtpEmailSender>();
        var descriptor2 = ServiceDescriptor.Scoped<IOrderRepository, EfOrderRepository>();
        var descriptor3 = ServiceDescriptor.Singleton<ICacheService>(new MemoryCacheService());
        var descriptor4 = ServiceDescriptor.Transient<ILogger>(sp => 
            new Logger(sp.GetRequiredService<ILoggerFactory>().CreateLogger("Default")));

Internal процесс резолвинга

// Упрощённая схема работы IServiceProvider
        internal class ServiceProvider
        {
            private readonly Service[] _services;
            private readonly ConcurrentDictionary<Type, object> _singletonCache = new();
            private readonly AsyncLocal<ScopedServiceProvider> _currentScope = new();

            public object GetService(Type serviceType)
            {
                // 1. Проверяем singleton cache
                // 2. Создаём новый экземпляр в зависимости от lifetime
                // 3. Рекурсивно резолвим зависимости конструктора
                // 4. Возвращаем экземпляр
            }
        }

Ключевые моменты:

  • Singleton создаётся лениво при первом запросе (если не используется AddSingleton(instance))
  • Scoped создаётся один раз на IServiceScope
  • Transient создаётся при каждом запросе
  • Граф зависимостей строится рекурсивно — все зависимости резолвятся до создания root объекта

Service Lifetimes

Transient

Создаётся каждый раз при запросе из контейнера.

builder.Services.AddTransient<ITransientService, TransientService>();

        // Каждый вызов GetService возвращает новый экземпляр
        var s1 = provider.GetRequiredService<ITransientService>();
        var s2 = provider.GetRequiredService<ITransientService>();
        // s1 != s2 — разные объекты

Когда использовать:

  • Лёгкие stateless сервисы
  • Сервисы, которые не хранят состояние
  • Когда каждый пользователь/запрос должен получить свой экземпляр

Когда НЕ использовать:

  • Дорогие для создания объекты (подключение к БД, HTTP клиент)
  • Сервисы, которые должны разделять состояние

Scoped

Один экземпляр на scope (в ASP.NET — один на HTTP запрос).

builder.Services.AddScoped<IScopedService, ScopedService>();

        // В рамках одного scope — один экземпляр
        using var scope = provider.CreateScope();
        var s1 = scope.ServiceProvider.GetRequiredService<IScopedService>();
        var s2 = scope.ServiceProvider.GetRequiredService<IScopedService>();
        // s1 == s2 — один объект

        // В другом scope — другой экземпляр
        using var scope2 = provider.CreateScope();
        var s3 = scope2.ServiceProvider.GetRequiredService<IScopedService>();
        // s1 != s3 — разные объекты

Когда использовать:

  • DbContext (один на запрос — единый Unit of Work)
  • Сервисы, которые хранят состояние запроса
  • Сервисы, работающие с данными текущего пользователя

Singleton

Один экземпляр на всё время жизни приложения.

builder.Services.AddSingleton<ISingletonService, SingletonService>();

        // Всегда один экземпляр
        var s1 = provider.GetRequiredService<ISingletonService>();
        var s2 = provider.GetRequiredService<ISingletonService>();
        // s1 == s2 — один объект на всё приложение

Когда использовать:

  • Кэши
  • Конфигурация
  • Сервисы без состояния, дорогие для создания
  • Background services

Когда НЕ использовать:

  • Сервисы, зависящие от Scoped/Transient (captive dependency)
  • Сервисы, которые хранят per-request состояние

Captive Dependencies Problem

Проблема: Singleton захватывает Scoped сервис, и Scoped сервис живёт как Singleton.

// Captive dependency — ОШИБКА!
        public class ReportGenerator // Singleton
        {
            private readonly AppDbContext _context; // Scoped!

            public ReportGenerator(AppDbContext context)
            {
                _context = context; // DbContext теперь живёт как Singleton!
            }
        }

        // Регистрация
        builder.Services.AddSingleton<IReportGenerator, ReportGenerator>();
        builder.Services.AddScoped<AppDbContext>(); // Будет захвачен!

        // Проблема: DbContext не будет disposed после запроса,
        // будет копить tracked entities, memory leak

Решения:

// Решение 1: Изменить lifetime на Singleton (если возможно)
        builder.Services.AddSingleton<IReportGenerator, ReportGenerator>();
        builder.Services.AddSingleton<AppDbContext>(); // Но это плохо для DbContext

        // Решение 2: Использовать Factory для создания scope
        public class ReportGenerator : IReportGenerator
        {
            private readonly IServiceProvider _serviceProvider;

            public ReportGenerator(IServiceProvider serviceProvider)
            {
                _serviceProvider = serviceProvider;
            }

            public async Task GenerateReportAsync()
            {
                // Создаём scope вручную
                using var scope = _serviceProvider.CreateScope();
                var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        
                // Используем context в рамках scope
                var reports = await context.Reports.ToListAsync();
            }
        }

        // Решение 3: Inject Func<T> или IServiceProvider
        public class OrderProcessor
        {
            private readonly Func<AppDbContext> _dbContextFactory;

            public OrderProcessor(IServiceProvider sp)
            {
                _dbContextFactory = () => sp.GetRequiredService<AppDbContext>();
            }

            public async Task ProcessAsync()
            {
                using var scope = _serviceProvider.CreateScope();
                var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                // ...
            }
        }

Factory Patterns

Func Factory

// Регистрация Func<T>
        builder.Services.AddTransient<ISmsSender, TwilioSmsSender>();
        builder.Services.AddTransient<Func<ISmsSender>>(sp => () => sp.GetRequiredService<ISmsSender>());

        // Использование
        public class NotificationService
        {
            private readonly Func<ISmsSender> _smsFactory;

            public NotificationService(Func<ISmsSender> smsFactory)
            {
                _smsFactory = smsFactory;
            }

            public async Task SendNotificationsAsync(IEnumerable<string> phoneNumbers)
            {
                foreach (var phone in phoneNumbers)
                {
                    // Каждый раз новый экземпляр
                    using var scope = new ServiceScope(); // если нужно
                    var sender = _smsFactory();
                    await sender.SendAsync(phone, "Hello!");
                }
            }
        }

ActivatorUtilities

// ActivatorUtilities позволяет создавать объекты с DI + runtime параметрами
        public class OrderFactory
        {
            private readonly IServiceProvider _serviceProvider;

            public OrderFactory(IServiceProvider serviceProvider)
            {
                _serviceProvider = serviceProvider;
            }

            public Order CreateOrder(int customerId, IEnumerable<OrderItem> items)
            {
                // DI резолвит IOrderValidator, IOrderCalculator
                // customerId и items передаются вручную
                return ActivatorUtilities.CreateInstance<Order>(
                    _serviceProvider,
                    customerId,
                    items
                );
            }
        }

        // Order должен иметь конструктор, совместимый с параметрами
        public class Order
        {
            public Order(
                IOrderValidator validator,  // Из DI
                IOrderCalculator calculator, // Из DI
                int customerId,              // Runtime параметр
                IEnumerable<OrderItem> items) // Runtime параметр
            {
                // ...
            }
        }

IFactory — кастомная фабрика

public interface IFactory<out T>
        {
            T Create();
        }

        public class HttpClientFactory : IFactory<HttpClient>
        {
            private readonly string _baseUrl;
            private readonly HttpMessageHandler _handler;

            public HttpClientFactory(string baseUrl, HttpMessageHandler handler)
            {
                _baseUrl = baseUrl;
                _handler = handler;
            }

            public HttpClient Create()
            {
                var client = new HttpClient(_handler);
                client.BaseAddress = new Uri(_baseUrl);
                return client;
            }
        }

        // Регистрация
        builder.Services.AddSingleton<HttpMessageHandler, SocketsHttpHandler>();
        builder.Services.AddTransient<IFactory<HttpClient>>(sp => 
            new HttpClientFactory("https://api.example.com", sp.GetRequiredService<HttpMessageHandler>()));

Open Generic Registrations

// Open Generic — регистрация для любого типа параметра
        builder.Services(typeof(IRepository<>), typeof(EfRepository<>));

        // Работает для любого TEntity
        var orderRepo = provider.GetRequiredService<IRepository<Order>>();
        var productRepo = provider.GetRequiredService<IRepository<Product>>();
        var userRepo = provider.GetRequiredService<IRepository<User>>();

        // С ограничениями
        public interface IValidator<T> where T : class { }
        public class FluentValidator<T> : IValidator<T> where T : class { }

        builder.Services(typeof(IValidator<>), typeof(FluentValidator<>));

        // Multiple implementations
        builder.Services.AddTransient(typeof(IValidator<>), typeof(OrderValidator<>));
        builder.Services.AddTransient(typeof(IValidator<>), typeof(ProductValidator<>));

        // Получение всех валидаторов
        var validators = provider.GetServices<IValidator<Order>>();

Generic с несколькими параметрами

public interface ICommandHandler<TCommand, TResult> { }
        public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, OrderResult> { }

        // Регистрация конкретного handler
        builder.Services.AddTransient<ICommandHandler<CreateOrderCommand, OrderResult>, CreateOrderHandler>();

        // Или open generic
        public interface IQueryHandler<TQuery, TResult> { }
        public class EfQueryHandler<TQuery, TResult> : IQueryHandler<TQuery, TResult> { }

        builder.Services(typeof(IQueryHandler<,>), typeof(EfQueryHandler<,>));

DI в Background Services

Scoped в Background Service

public class OrderCleanupService : BackgroundService
        {
            private readonly IServiceProvider _serviceProvider;
            private readonly ILogger<OrderCleanupService> _logger;

            public OrderCleanupService(
                IServiceProvider serviceProvider,
                ILogger<OrderCleanupService> logger)
            {
                _serviceProvider = serviceProvider;
                _logger = logger;
            }

            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    // Создаём scope для каждой итерации
                    using var scope = _serviceProvider.CreateScope();
                    var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                    var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();

                    var oldOrders = await dbContext.Orders
                        .Where(o => o.CreatedAt < DateTime.UtcNow.AddDays(-30))
                        .ToListAsync(stoppingToken);

                    foreach (var order in oldOrders)
                    {
                        await emailService.SendAsync(order.CustomerEmail, "Order archived");
                        dbContext.Orders.Remove(order);
                    }

                    await dbContext.SaveChangesAsync(stoppingToken);
            
                    await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
                }
            }
        }

Кастомный Middleware для DI Diagnostics

public class DiagnosticsMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly ILogger<DiagnosticsMiddleware> _logger;

            public DiagnosticsMiddleware(RequestDelegate next, ILogger<DiagnosticsMiddleware> logger)
            {
                _next = next;
                _logger = logger;
            }

            public async Task InvokeAsync(HttpContext context, IServiceProvider serviceProvider)
            {
                var stopwatch = Stopwatch.StartNew();
        
                try
                {
                    await _next(context);
                }
                finally
                {
                    stopwatch.Stop();
            
                    // Логируем время резолвинга сервисов
                    _logger.LogInformation(
                        "Request {Method} {Path} completed in {Elapsed}ms",
                        context.Request.Method,
                        context.Request.Path,
                        stopwatch.ElapsedMilliseconds
                    );

                    // Проверка на captive dependencies
                    if (context.RequestServices is ServiceProvider sp)
                    {
                        // Можно добавить кастомную диагностику
                    }
                }
            }
        }

Decorator Pattern через DI Registration

Вручную

// Обёртываем вручную
        builder.Services.AddSingleton<IEmailService>(sp =>
        {
            var inner = new SmtpEmailService();
            var logger = sp.GetRequiredService<ILogger<LoggingEmailDecorator>>();
            return new LoggingEmailDecorator(inner, logger);
        });

Через Scrutor

// NuGet: Scrutor
        builder.Services.AddSingleton<IEmailService, SmtpEmailService>();
        builder.Services.Decorate<IEmailService, LoggingEmailDecorator>();
        builder.Services.Decorate<IEmailService, RetryEmailDecorator>();

        // Порядок: RetryEmailDecorator(LoggingEmailDecorator(SmtpEmailService))

Кастомный Extension Method

public static class ServiceCollectionExtensions
        {
            public static IServiceCollection Decorate<TService, TDecorator>(this IServiceCollection services)
                where TDecorator : TService
            {
                var descriptors = services.Where(d => d.ServiceType == typeof(TService)).ToList();
        
                foreach (var descriptor in descriptors)
                {
                    services.Remove(descriptor);
                }

                services.Add(new ServiceDescriptor(typeof(TService), sp =>
                {
                    // Создаём inner service из оригинального descriptor
                    var inner = CreateService(sp, descriptor);
            
                    // Создаём decorator, inject'им inner
                    return ActivatorUtilities.CreateInstance<TDecorator>(sp, inner);
                }, descriptor.Lifetime));

                return services;
            }

            private static object CreateService(IServiceProvider sp, ServiceDescriptor descriptor)
            {
                if (descriptor.ImplementationInstance != null)
                    return descriptor.ImplementationInstance;
        
                if (descriptor.ImplementationFactory != null)
                    return descriptor.ImplementationFactory(sp);
        
                return ActivatorUtilities.CreateInstance(sp, descriptor.ImplementationType);
            }
        }

Validation Service Provider

// Валидация DI контейнера при startup
        public static class ServiceCollectionExtensions
        {
            public static IServiceProvider BuildValidatingServiceProvider(this IServiceCollection services)
            {
                var provider = services.BuildServiceProvider();
        
                // Проверяем все зарегистрированные сервисы
                var serviceTypes = services
                    .Where(d => d.ImplementationType != null)
                    .Select(d => d.ImplementationType)
                    .Distinct();

                foreach (var type in serviceTypes)
                {
                    try
                    {
                        // Пытаемся создать — если не получится, узнаем на startup
                        ActivatorUtilities.CreateInstance(provider, type);
                    }
                    catch (Exception ex)
                    {
                        throw new InvalidOperationException(
                            $"Failed to create {type.Name}: {ex.Message}", ex);
                    }
                }

                return provider;
            }
        }

Практика

Задание 1: Scoped-per-Request в Background Service

Реализовать BackgroundService, который создаёт scope для каждой итерации и корректно работает с Scoped сервисами (DbContext, etc.).

Задание 2: Кастомный Middleware для DI Diagnostics

Создать middleware, который логирует:

  • Время резолвинга сервисов
  • Количество созданных Transient экземпляров за запрос
  • Предупреждения о потенциальных captive dependencies

Задание 3: Decorator через DI

Реализовать систему декораторов, которая позволяет навешивать декораторы на любой сервис через fluent API:

builder.Services
            .AddSingleton<IEmailService, SmtpEmailService>()
            .WithDecorator<LoggingDecorator>()
            .WithDecorator<RetryDecorator>()
            .WithDecorator<QueueDecorator>();

Архитектурные стили

Layered Architecture (N-Tier)

Классическая трёхзвенная архитектура

┌─────────────────────────────────────────┐
        │         Presentation Layer              │
        │     (Controllers, Views, API)           │
        ├─────────────────────────────────────────┤
        │          Business Logic Layer           │
        │    (Services, Validators, Rules)        │
        ├─────────────────────────────────────────┤
        │         Data Access Layer               │
        │   (Repositories, DbContext, SQL)        │
        └─────────────────────────────────────────┘

Проблемы:

  • Зависимости направлены вниз — UI зависит от Business, Business зависит от Data
  • Сложно тестировать Business Layer без Data Layer
  • Domain logic "протекает" в инфраструктуру
  • При изменении БД приходится менять Business Layer
// Проблема: Business Layer зависит от Entity Framework
        public class OrderService
        {
            private readonly AppDbContext _context; // Прямая зависимость от EF!

            public OrderService(AppDbContext context)
            {
                _context = context;
            }

            public async Task<Order> GetOrderAsync(int id)
            {
                return await _context.Orders
                    .Include(o => o.Items)
                    .FirstOrDefaultAsync(o => o.Id == id);
            }
        }

Clean Architecture

Принципы

Автор: Robert C. Martin (Uncle Bob)

Ключевая идея: Зависимости направлены внутрь. Внешние слои зависят от внутренних, но не наоборот.

┌─────────────────────────────────────────────────────────┐
        │                    Frameworks & Drivers                  │
        │              (UI, DB, External APIs, CLI)                │
        ├─────────────────────────────────────────────────────────┤
        │                   Interface Adapters                     │
        │            (Controllers, Presenters, Gateways)           │
        ├─────────────────────────────────────────────────────────┤
        │                    Use Cases                             │
        │           (Application Services, Interactors)            │
        ├─────────────────────────────────────────────────────────┤
        │                    Entities                              │
        │              (Domain Models, Business Rules)             │
        └─────────────────────────────────────────────────────────┘

Правило зависимостей: Исходный код зависимостей указывает только внутрь.

Структура проекта

src/
        ├── Domain/                    # Inner layer — Entities, Value Objects, Domain Events
        │   ├── Entities/
        │   ├── ValueObjects/
        │   ├── Events/
        │   ├── Exceptions/
        │   └── Domain.csproj
        │
        ├── Application/               # Use Cases — Interfaces, DTOs, Services
        │   ├── Interfaces/            # Repository interfaces, Service interfaces
        │   ├── DTOs/
        │   ├── Commands/              # CQRS Commands
        │   ├── Queries/               # CQRS Queries
        │   ├── Handlers/
        │   └── Application.csproj     # Depends on: Domain
        │
        ├── Infrastructure/            # Outer layer — Implementations
        │   ├── Persistence/           # DbContext, Repositories
        │   ├── Services/              # Email, SMS, Payment implementations
        │   ├── ExternalApis/
        │   └── Infrastructure.csproj  # Depends on: Application
        │
        └── WebApi/                    # Presentation layer
            ├── Controllers/
            ├── Middleware/
            └── WebApi.csproj          # Depends on: Application, Infrastructure

Пример реализации

Domain Layer
// Entity
        public class Order : Entity
        {
            private readonly List<OrderItem> _items = new();
    
            public OrderId Id { get; private set; }
            public CustomerId CustomerId { get; private set; }
            public OrderStatus Status { get; private set; }
            public DateTime CreatedAt { get; private set; }
            public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
            public Money TotalAmount { get; private set; }

            private Order() { } // EF Core

            public Order(CustomerId customerId)
            {
                CustomerId = customerId ?? throw new ArgumentNullException(nameof(customerId));
                Id = OrderId.New();
                Status = OrderStatus.Draft;
                CreatedAt = DateTime.UtcNow;
                TotalAmount = Money.Zero;
        
                AddDomainEvent(new OrderCreatedEvent(Id, CustomerId));
            }

            public void AddItem(ProductId productId, int quantity, Money price)
            {
                if (Status != OrderStatus.Draft)
                    throw new DomainException("Cannot modify non-draft order");
        
                if (quantity <= 0)
                    throw new DomainException("Quantity must be positive");

                var item = new OrderItem(Id, productId, quantity, price);
                _items.Add(item);
                RecalculateTotal();
            }

            public void Confirm()
            {
                if (Status != OrderStatus.Draft)
                    throw new DomainException("Order must be in Draft status");
        
                if (_items.Count == 0)
                    throw new DomainException("Order must have at least one item");

                Status = OrderStatus.Confirmed;
                AddDomainEvent(new OrderConfirmedEvent(Id));
            }

            private void RecalculateTotal()
            {
                TotalAmount = _items.Sum(i => i.LineTotal);
            }
        }

        // Value Object
        public readonly record struct Money(decimal Amount, string Currency)
        {
            public static Money Zero => new(0, "USD");
    
            public Money Add(Money other)
            {
                if (Currency != other.Currency)
                    throw new InvalidOperationException("Cannot add different currencies");
                return new Money(Amount + other.Amount, Currency);
            }
        }

        public readonly record struct OrderId(Guid Value)
        {
            public static OrderId New() => new(Guid.NewGuid());
        }

        // Domain Event
        public record OrderCreatedEvent(OrderId OrderId, CustomerId CustomerId) : IDomainEvent;
        public record OrderConfirmedEvent(OrderId OrderId) : IDomainEvent;
Application Layer
// Interface (порт)
        public interface IOrderRepository
        {
            Task<Order> GetByIdAsync(OrderId id, CancellationToken ct = default);
            Task AddAsync(Order order, CancellationToken ct = default);
            Task UpdateAsync(Order order, CancellationToken ct = default);
        }

        // Command
        public record CreateOrderCommand(CustomerId CustomerId) : IRequest<OrderResult>;
        public record AddOrderItemCommand(OrderId OrderId, ProductId ProductId, int Quantity) : IRequest;

        // Handler
        public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, OrderResult>
        {
            private readonly IOrderRepository _repository;
            private readonly IDomainEventPublisher _eventPublisher;

            public CreateOrderHandler(IOrderRepository repository, IDomainEventPublisher eventPublisher)
            {
                _repository = repository;
                _eventPublisher = eventPublisher;
            }

            public async Task<OrderResult> Handle(CreateOrderCommand request, CancellationToken ct)
            {
                var order = new Order(request.CustomerId);
                await _repository.AddAsync(order, ct);
                await _repository.UnitOfWork.SaveChangesAsync(ct);

                await _eventPublisher.PublishAsync(order.DomainEvents, ct);
        
                return new OrderResult(order.Id.Value, order.Status.ToString());
            }
        }
Infrastructure Layer
// Реализация репозитория
        public class EfOrderRepository : IOrderRepository
        {
            private readonly AppDbContext _context;

            public EfOrderRepository(AppDbContext context) => _context = context;

            public IUnitOfWork UnitOfWork => _context;

            public async Task<Order> GetByIdAsync(OrderId id, CancellationToken ct = default)
            {
                return await _context.Orders
                    .Include(o => o.Items)
                    .FirstOrDefaultAsync(o => o.Id.Value == id.Value, ct);
            }

            public async Task AddAsync(Order order, CancellationToken ct = default)
            {
                await _context.Orders.AddAsync(order, ct);
            }

            public Task UpdateAsync(Order order, CancellationToken ct = default)
            {
                _context.Orders.Update(order);
                return Task.CompletedTask;
            }
        }

Onion Architecture

Принципы

Автор: Jeffrey Palermo (2008)

По сути аналогична Clean Architecture, но с другим визуальным представлением:

┌──────────────────────────────────────────────────────┐
        │                  Infrastructure                       │
        │  ┌────────────────────────────────────────────────┐  │
        │  │              Application Services               │  │
        │  │  ┌──────────────────────────────────────────┐  │  │
        │  │  │            Domain Models                  │  │  │
        │  │  │   (Entities, Value Objects, Interfaces)   │  │  │
        │  │  └──────────────────────────────────────────┘  │  │
        │  └────────────────────────────────────────────────┘  │
        └──────────────────────────────────────────────────────┘

Отличие от Clean Architecture: Onion Architecture делает больший акцент на Domain как на центре всего. Clean Architecture более общая и может применяться к non-domain-centric системам.

Структура проекта

src/
        ├── Domain/
        │   ├── Entities/
        │   ├── ValueObjects/
        │   ├── Aggregates/
        │   ├── Events/
        │   └── Interfaces/          # Repository interfaces здесь!
        │
        ├── Application/
        │   ├── Services/
        │   ├── DTOs/
        │   ├── Validators/
        │   └── Mappings/
        │
        ├── Infrastructure/
        │   ├── Data/
        │   ├── Repositories/        # Реализации интерфейсов из Domain
        │   ├── ExternalServices/
        │   └── Messaging/
        │
        └── Web/
            ├── Controllers/
            ├── ViewModels/
            └── wwwroot/

Hexagonal Architecture (Ports and Adapters)

Принципы

Автор: Alistair Cockburn (2005)

Ключевая идея: Приложение — это шестиугольник (hexagon), окружённый портами (interfaces) и адаптерами (implementations).

                    ┌──────────────┐
                            │  REST API    │  ← Driving Adapter
                            └──────┬───────┘
                                   │
        ┌──────────────┐   ┌───────▼────────┐   ┌──────────────┐
        │   Web UI     │──▶│                │   │   Database   │
        └──────────────┘   │                │   └──────┬───────┘
                           │    Hexagon     │◀─────────┘
        ┌──────────────┐   │  (Application  │   ┌──────┴───────┐
        │   CLI        │──▶│    + Domain)   │   │  Message     │
        └──────────────┘   │                │   │  Broker      │
                           │                │   └──────────────┘
        ┌──────────────┐   │                │
        │   Tests      │──▶│                │
        └──────────────┘   └────────────────┘

Порты:

  • Driving (Primary): Входы в систему — HTTP запросы, CLI команды, события
  • Driven (Secondary): Выходы из системы — БД, внешние API, файловая система

Пример реализации

// === DOMAIN (центр шестиугольника) ===
        public class Order
        {
            public Guid Id { get; }
            public string CustomerId { get; }
            public List<OrderLine> Lines { get; }
            public OrderStatus Status { get; private set; }

            public void Confirm() => Status = OrderStatus.Confirmed;
        }

        // === PORTS (интерфейсы на границе) ===

        // Driving Port — как внешний мир взаимодействует с системой
        public interface IOrderService
        {
            Task<OrderDto> CreateOrderAsync(CreateOrderRequest request);
            Task<OrderDto> GetOrderAsync(Guid id);
            Task ConfirmOrderAsync(Guid id);
        }

        // Driven Ports — как система взаимодействует с внешним миром
        public interface IOrderStore
        {
            Task<Order> GetByIdAsync(Guid id);
            Task SaveAsync(Order order);
        }

        public interface IPaymentGateway
        {
            Task<PaymentResult> ChargeAsync(decimal amount, string currency);
        }

        public interface IEmailNotifier
        {
            Task SendOrderConfirmationAsync(string email, Guid orderId);
        }

        // === ADAPTERS (реализации портов) ===

        // Driving Adapter — REST API
        [ApiController]
        [Route("api/orders")]
        public class OrdersController : ControllerBase
        {
            private readonly IOrderService _orderService;

            public OrdersController(IOrderService orderService) => _orderService = orderService;

            [HttpPost]
            public async Task<ActionResult<OrderDto>> CreateOrder(CreateOrderRequest request)
            {
                var order = await _orderService.CreateOrderAsync(request);
                return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
            }
        }

        // Driven Adapter — EF Core Repository
        public class EfOrderStore : IOrderStore
        {
            private readonly AppDbContext _context;

            public EfOrderStore(AppDbContext context) => _context = context;

            public async Task<Order> GetByIdAsync(Guid id) =>
                await _context.Orders.FindAsync(id);

            public async Task SaveAsync(Order order)
            {
                _context.Orders.Update(order);
                await _context.SaveChangesAsync();
            }
        }

        // Driven Adapter — Stripe Payment
        public class StripePaymentGateway : IPaymentGateway
        {
            private readonly StripeClient _client;

            public async Task<PaymentResult> ChargeAsync(decimal amount, string currency)
            {
                // Stripe API call
                return new PaymentResult { Success = true, TransactionId = "..." };
            }
        }

Domain-Driven Design (DDD)

Strategic Design

Bounded Contexts

Определение: Граница, внутри которой определённая модель имеет конкретное значение.

┌─────────────────┐   ┌─────────────────┐   ┌─────────────────┐
        │    Sales        │   │   Shipping      │   │   Billing       │
        │                 │   │                 │   │                 │
        │  Order (Sales)  │──▶│  Order (Ship)   │──▶│  Invoice        │
        │  - items        │   │  - packages     │   │  - amount       │
        │  - customer     │   │  - address      │   │  - tax          │
        │  - total        │   │  - carrier      │   │  - status       │
        └─────────────────┘   └─────────────────┘   └─────────────────┘

Важно: Order в Sales и Order в Shipping — это разные модели с разными атрибутами и поведением.

Ubiquitous Language

Определение: Общий язык, используемый разработчиками и экспертами предметной области.

// Плохо: технические термины в domain
        public class OrderEntity
        {
            public int Id { get; set; }
            public DateTime Timestamp { get; set; }
            public List<OrderItemEntity> Children { get; set; }
        }

        // Хорошо: язык бизнеса
        public class Order
        {
            public OrderNumber Number { get; }
            public DateTime PlacedAt { get; }
            public IReadOnlyList<OrderLine> Lines { get; }
    
            public void AddLine(Product product, Quantity quantity) { ... }
            public void Cancel(CancellationReason reason) { ... }
        }
Context Map

Паттерны интеграции между Bounded Contexts:

ПаттернОписаниеКогда использовать
Shared KernelОбщий код между контекстамиМаленькие команды, тесная связь
Customer-SupplierОдин контекст зависит от другогоЧёткая зависимость
ConformistDownstream принимает модель upstreamНет возможности изменить upstream
Anti-Corruption LayerТрансформация модели при интеграцииLegacy системы, разные модели
Open Host ServiceПубликация API для многих consumersPlatform, SaaS
Published LanguageОбщий формат данных (XML, JSON Schema)Стандартизированная интеграция

Tactical Design

Entities
// Entity — имеет идентичность, изменяется во времени
        public class Customer : Entity
        {
            public CustomerId Id { get; private set; }
            public string Name { get; private set; }
            public Email Email { get; private set; }
            public Address Address { get; private set; }
            public CustomerStatus Status { get; private set; }

            private Customer() { }

            public Customer(string name, Email email)
            {
                Name = name ?? throw new ArgumentNullException(nameof(name));
                Email = email ?? throw new ArgumentNullException(nameof(email));
                Id = CustomerId.New();
                Status = CustomerStatus.Active;
            }

            public void ChangeAddress(Address newAddress)
            {
                Address = newAddress ?? throw new ArgumentNullException(nameof(newAddress));
                AddDomainEvent(new CustomerAddressChangedEvent(Id, newAddress));
            }

            public void Suspend(string reason)
            {
                if (Status == CustomerStatus.Suspended)
                    throw new DomainException("Customer already suspended");
        
                Status = CustomerStatus.Suspended;
                AddDomainEvent(new CustomerSuspendedEvent(Id, reason));
            }
        }
Value Objects
// Value Object — определяется значениями, неизменяем
        public readonly record struct Address
        {
            public string Street { get; }
            public string City { get; }
            public string State { get; }
            public string ZipCode { get; }
            public string Country { get; }

            public Address(string street, string city, string state, string zipCode, string country)
            {
                if (string.IsNullOrWhiteSpace(street)) throw new ArgumentException("Street is required");
                if (string.IsNullOrWhiteSpace(city)) throw new ArgumentException("City is required");
                if (!ZipCodeRegex.IsMatch(zipCode)) throw new ArgumentException("Invalid ZIP code");
        
                Street = street;
                City = city;
                State = state;
                ZipCode = zipCode;
                Country = country;
            }
        }

        // Value Object с бизнес-логикой
        public readonly record struct Money
        {
            public decimal Amount { get; }
            public string Currency { get; }

            public Money(decimal amount, string currency = "USD")
            {
                if (amount < 0) throw new ArgumentException("Amount cannot be negative");
                if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentException("Currency is required");
        
                Amount = amount;
                Currency = currency.ToUpperInvariant();
            }

            public static Money Zero => new(0);
            public static Money FromDecimal(decimal amount) => new(amount);

            public Money Add(Money other)
            {
                if (Currency != other.Currency)
                    throw new InvalidOperationException($"Cannot add {Currency} and {other.Currency}");
                return new Money(Amount + other.Amount, Currency);
            }

            public Money Multiply(decimal factor) => new(Amount * factor, Currency);
        }
Aggregates и Aggregate Roots
// Aggregate Root — Order контролирует все свои OrderItem
        public class Order : AggregateRoot
        {
            private readonly List<OrderItem> _items = new();

            public OrderId Id { get; private set; }
            public CustomerId CustomerId { get; private set; }
            public OrderStatus Status { get; private set; }
            public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
            public Money Total { get; private set; }

            private Order() { }

            public Order(CustomerId customerId)
            {
                Id = OrderId.New();
                CustomerId = customerId;
                Status = OrderStatus.Draft;
                Total = Money.Zero;
            }

            // Все изменения OrderItem проходят через Order
            public void AddItem(Product product, int quantity)
            {
                GuardDraft();
        
                if (quantity <= 0) throw new DomainException("Quantity must be positive");
                if (quantity > product.MaxOrderQuantity) 
                    throw new DomainException($"Max order quantity is {product.MaxOrderQuantity}");

                var item = new OrderItem(Id, product.Id, quantity, product.Price);
                _items.Add(item);
                RecalculateTotal();
            }

            public void RemoveItem(OrderItemId itemId)
            {
                GuardDraft();
        
                var item = _items.FirstOrDefault(i => i.Id == itemId);
                if (item == null) throw new DomainException("Item not found");
        
                _items.Remove(item);
                RecalculateTotal();
            }

            public void Confirm()
            {
                if (Status != OrderStatus.Draft)
                    throw new DomainException("Only draft orders can be confirmed");
        
                if (!_items.Any())
                    throw new DomainException("Cannot confirm empty order");

                Status = OrderStatus.Confirmed;
                AddDomainEvent(new OrderConfirmedEvent(Id, CustomerId, Total));
            }

            private void GuardDraft()
            {
                if (Status != OrderStatus.Draft)
                    throw new DomainException($"Cannot modify order in {Status} status");
            }

            private void RecalculateTotal()
            {
                Total = _items.Aggregate(Money.Zero, (sum, item) => sum.Add(item.LineTotal));
            }
        }

        // OrderItem — не Aggregate Root, не может существовать без Order
        public class OrderItem : Entity
        {
            public OrderItemId Id { get; private set; }
            public OrderId OrderId { get; private set; }
            public ProductId ProductId { get; private set; }
            public int Quantity { get; private set; }
            public Money UnitPrice { get; private set; }
            public Money LineTotal => UnitPrice.Multiply(Quantity);

            internal OrderItem(OrderId orderId, ProductId productId, int quantity, Money unitPrice)
            {
                Id = OrderItemId.New();
                OrderId = orderId;
                ProductId = productId;
                Quantity = quantity;
                UnitPrice = unitPrice;
            }
        }
Domain Events
// Базовый класс для Domain Events
        public interface IDomainEvent
        {
            DateTime OccurredOn { get; }
        }

        public abstract class DomainEvent : IDomainEvent
        {
            public DateTime OccurredOn { get; } = DateTime.UtcNow;
        }

        // Entity с поддержкой Domain Events
        public abstract class Entity
        {
            private readonly List<IDomainEvent> _domainEvents = new();
            public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

            protected void AddDomainEvent(IDomainEvent domainEvent)
            {
                _domainEvents.Add(domainEvent);
            }

            public void ClearDomainEvents()
            {
                _domainEvents.Clear();
            }
        }

        // Event Handler
        public interface IDomainEventHandler<TEvent> where TEvent : IDomainEvent
        {
            Task HandleAsync(TEvent @event, CancellationToken ct = default);
        }

        // Конкретный handler
        public class OrderConfirmedEmailHandler : IDomainEventHandler<OrderConfirmedEvent>
        {
            private readonly IEmailService _email;
            private readonly ICustomerRepository _customers;

            public async Task HandleAsync(OrderConfirmedEvent @event, CancellationToken ct = default)
            {
                var customer = await _customers.GetByIdAsync(@event.CustomerId, ct);
                await _email.SendAsync(customer.Email, "Order Confirmed", 
                    $"Your order {@event.OrderId} for {@event.Total} has been confirmed");
            }
        }

        // Публикация событий
        public class DomainEventPublisher : IDomainEventPublisher
        {
            private readonly IServiceProvider _serviceProvider;

            public async Task PublishAsync(IEnumerable<IDomainEvent> events, CancellationToken ct = default)
            {
                foreach (var @event in events)
                {
                    var eventType = @event.GetType();
                    var handlerType = typeof(IDomainEventHandler<>).MakeGenericType(eventType);
            
                    var handlers = _serviceProvider.GetServices(handlerType);
            
                    foreach (var handler in handlers)
                    {
                        var method = handlerType.GetMethod("HandleAsync");
                        await (Task)method.Invoke(handler, new object[] { @event, ct });
                    }
                }
            }
        }
Domain Services
// Domain Service — логика, которая не принадлежит одному Entity
        public class OrderPricingService
        {
            private readonly IDiscountPolicy _discountPolicy;
            private readonly ITaxCalculator _taxCalculator;

            public OrderPricingService(IDiscountPolicy discountPolicy, ITaxCalculator taxCalculator)
            {
                _discountPolicy = discountPolicy;
                _taxCalculator = taxCalculator;
            }

            public OrderTotal CalculateTotal(Order order, Customer customer)
            {
                var subtotal = order.Items.Sum(i => i.LineTotal.Amount);
                var discount = _discountPolicy.CalculateDiscount(order, customer);
                var taxableAmount = subtotal - discount;
                var tax = _taxCalculator.Calculate(taxableAmount, customer.Address);
        
                return new OrderTotal(subtotal, discount, tax);
            }
        }

Repositories vs Direct Database Access

// Repository — абстракция коллекции Aggregate Root
        public interface IOrderRepository
        {
            Task<Order> GetByIdAsync(OrderId id, CancellationToken ct = default);
            Task<IReadOnlyList<Order>> GetByCustomerAsync(CustomerId customerId, CancellationToken ct = default);
            Task AddAsync(Order order, CancellationToken ct = default);
            Task RemoveAsync(Order order, CancellationToken ct = default);
        }

        // Почему Repository, а не прямой доступ к БД:
        // 1. Domain layer не знает о EF Core / Dapper / etc.
        // 2. Можно легко заменить реализацию
        // 3. Можно замокать для тестирования
        // 4. Repository выражает намерение — "коллекция Order"
        // 5. Скрывает детали persistence от domain

        // Anti-pattern: Repository для всего
        public interface IGenericRepository<T> { ... } // Не делайте так!

        // Лучше: конкретные repository для каждого Aggregate Root
        public interface ICustomerRepository { ... }
        public interface IProductRepository { ... }
        // Нет IRepository<OrderItem> — OrderItem не Aggregate Root!

Практика

Задание 1: E-commerce Bounded Context

Спроектировать bounded context для e-commerce:

  • Определить Aggregate Roots (Order, Product, Customer)
  • Определить Value Objects (Money, Address, ProductId)
  • Определить Domain Events (OrderCreated, OrderShipped, PaymentReceived)
  • Определить границы контекстов (Sales, Inventory, Shipping, Billing)

Задание 2: Anti-Corruption Layer

Реализовать ACL для интеграции с legacy системой заказов:

  • Legacy система использует плоскую структуру данных
  • Новая система использует DDD модель
  • ACL трансформирует данные между моделями

Задание 3: Domain Model с Value Objects

Создать domain model для банковского счёта:

  • Value Objects: AccountNumber, Money, Currency, TransactionId
  • Entity: Account (Aggregate Root)
  • Invariant: баланс не может быть меньше овердрафта
  • Domain Events: Deposited, Withdrawn, AccountOpened, AccountClosed

Микросервисы vs Монолит

Когда микросервисы — правильный выбор

Монолит: когда он подходит

Монолит — это default choice. Начинайте с монолита.

Подходит, когда:

  • Команда < 10 человек
  • Domain не полностью понятен
  • Time-to-market критичен
  • Нет требований к независимому масштабированию
  • Простая deployment pipeline достаточна

Преимущества монолита:

  • Простота разработки и деплоя
  • Простое тестирование (in-memory, один процесс)
  • Простая отладка (один лог, один процесс)
  • ACID транзакции
  • Нет network latency между компонентами
  • Меньше operational overhead

Микросервисы: когда они нужны

Переходите к микросервисам, когда:

ФакторМонолитМикросервисы
Команда< 10 человек10+ человек, несколько команд
МасштабированиеВсё или ничегоТолько hot-spots
Time-to-marketБыстро для маленькой командыБыстро для больших организаций
ТехнологииОдин стекРазные стеки для разных задач
ОтказоустойчивостьSingle point of failureИзоляция отказов
DeploymentОдин артефактНезависимый деплой

Правило: Если вы не уверены, что вам нужны микросервисы — вам они не нужны.


Service Discovery, API Gateway, BFF

Service Discovery

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
        │   Service   │────▶│   Registry   │◀────│   Service   │
        │   A         │     │ (Consul,     │     │   B         │
        │  register   │     │  Eureka,     │     │  register   │
        │             │     │  K8s DNS)    │     │             │
        └─────────────┘     └──────┬───────┘     └─────────────┘
                                   │
                            ┌──────▼───────┐
                            │   Service    │
                            │   Consumer   │
                            │  (lookup)    │
                            └──────────────┘
Client-side Discovery
public class ServiceDiscoveryClient
        {
            private readonly HttpClient _httpClient;
            private readonly string _registryUrl;
            private ConcurrentDictionary<string, ServiceInstance> _cache = new();

            public async Task<ServiceInstance> ResolveAsync(string serviceName)
            {
                if (_cache.TryGetValue(serviceName, out var instance))
                {
                    if (instance.Healthy && instance.ExpiresAt > DateTime.UtcNow)
                        return instance;
                }

                var response = await _httpClient.GetAsync($"{_registryUrl}/services/{serviceName}");
                var instances = await response.Content.ReadFromJsonAsync<List<ServiceInstance>>();
        
                var healthy = instances.FirstOrDefault(i => i.Healthy);
                if (healthy == null) throw new ServiceUnavailableException(serviceName);

                _cache[serviceName] = healthy;
                return healthy;
            }
        }
Server-side Discovery (через Load Balancer)
Client → Load Balancer → Service Registry → Service Instance

В Kubernetes это делается через Service и DNS автоматически.


API Gateway

┌──────────────┐
        │    Client    │
        └──────┬───────┘
               │
               ▼
        ┌─────────────────────────────────┐
        │        API Gateway               │
        │  ┌───────────────────────────┐  │
        │  │  Authentication           │  │
        │  │  Rate Limiting            │  │
        │  │  Routing                  │  │
        │  │  Request/Response Transform│ │
        │  │  Aggregation              │  │
        │  └───────────────────────────┘  │
        └──────┬──────┬──────┬────────────┘
               │      │      │
               ▼      ▼      ▼
           ┌─────┐ ┌─────┐ ┌─────┐
           │UserService│ │OrderService│ │PaymentService│
           └─────┘ └─────┘ └─────┘
Реализация с YARP (Yet Another Reverse Proxy)
// NuGet: Yarp.ReverseProxy
        builder.Services.AddReverseProxy()
            .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
            .AddTransforms(builderContext =>
            {
                // Добавляем authentication header
                builderContext.AddRequestTransform(async transformContext =>
                {
                    var token = transformContext.HttpContext.Request.Headers["Authorization"];
                    transformContext.ProxyRequest.Headers.Add("X-Forwarded-Token", token);
                });
            });

        app.MapReverseProxy();
{
          "ReverseProxy": {
            "Routes": {
              "users-route": {
                "ClusterId": "users-cluster",
                "Match": { "Path": "/api/users/{**catch-all}" }
              },
              "orders-route": {
                "ClusterId": "orders-cluster",
                "Match": { "Path": "/api/orders/{**catch-all}" }
              }
            },
            "Clusters": {
              "users-cluster": {
                "Destinations": {
                  "destination1": { "Address": "http://user-service:8080/" }
                }
              },
              "orders-cluster": {
                "Destinations": {
                  "destination1": { "Address": "http://order-service:8080/" }
                }
              }
            }
          }
        }

BFF (Backend for Frontend)

┌─────────────┐     ┌─────────────┐
        │   Web App   │────▶│  Web BFF    │────▶ Microservices
        └─────────────┘     └─────────────┘
        ┌─────────────┐     ┌─────────────┐
        │ Mobile App  │────▶│ Mobile BFF  │────▶ Microservices
        └─────────────┘     └─────────────┘
        ┌─────────────┐     ┌─────────────┐
        │  Public API │────▶│  Public BFF │────▶ Microservices
        └─────────────┘     └─────────────┘

Зачем: Каждый клиент получает API, оптимизированный под его нужды.

// Web BFF — агрегирует данные из нескольких сервисов
        public class DashboardController : ControllerBase
        {
            private readonly IUserServiceClient _users;
            private readonly IOrderServiceClient _orders;
            private readonly INotificationServiceClient _notifications;

            [HttpGet("dashboard")]
            public async Task<DashboardDto> GetDashboard()
            {
                var userId = User.GetUserId();
        
                // Параллельные запросы к разным сервисам
                var (user, orders, notifications) = await Task.WhenAll(
                    _users.GetUserAsync(userId),
                    _orders.GetRecentOrdersAsync(userId, 10),
                    _notifications.GetUnreadAsync(userId)
                );

                return new DashboardDto
                {
                    User = user,
                    RecentOrders = orders,
                    UnreadNotifications = notifications.Count
                };
            }
        }

Inter-service Communication

Synchronous: gRPC

// order.proto
        syntax = "proto3";

        service OrderService {
          rpc GetOrder (GetOrderRequest) returns (GetOrderResponse);
          rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
          rpc StreamOrderUpdates (StreamOrderUpdatesRequest) returns (stream OrderUpdate);
        }

        message GetOrderRequest {
          string order_id = 1;
        }

        message GetOrderResponse {
          string order_id = 1;
          string customer_id = 2;
          repeated OrderItem items = 3;
          double total = 4;
          OrderStatus status = 5;
        }

        enum OrderStatus {
          DRAFT = 0;
          CONFIRMED = 1;
          SHIPPED = 2;
          DELIVERED = 3;
        }
// Server
        public class OrderService : OrderService.OrderServiceBase
        {
            private readonly IOrderRepository _repository;

            public OrderService(IOrderRepository repository) => _repository = repository;

            public override async Task<GetOrderResponse> GetOrder(
                GetOrderRequest request, ServerCallContext context)
            {
                var order = await _repository.GetByIdAsync(request.OrderId);
                if (order == null)
                    throw new RpcException(new Status(StatusCode.NotFound, "Order not found"));

                return new GetOrderResponse
                {
                    OrderId = order.Id.ToString(),
                    CustomerId = order.CustomerId.ToString(),
                    Total = order.Total.Amount,
                    Status = (OrderStatus)order.Status
                };
            }
        }

        // Client
        var channel = GrpcChannel.ForAddress("https://order-service:5001");
        var client = new OrderService.OrderServiceClient(channel);

        var response = await client.GetOrderAsync(new GetOrderRequest { OrderId = "123" });

Synchronous: HTTP/REST

// Typed HttpClient
        public class OrderServiceClient
        {
            private readonly HttpClient _http;

            public OrderServiceClient(HttpClient http)
            {
                _http = http;
                _http.BaseAddress = new Uri("https://order-service/api/");
            }

            public async Task<OrderDto> GetOrderAsync(string orderId, CancellationToken ct = default)
            {
                var response = await _http.GetAsync($"orders/{orderId}", ct);
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadFromJsonAsync<OrderDto>(ct);
            }
        }

        // Регистрация
        builder.Services.AddHttpClient<IOrderServiceClient, OrderServiceClient>();

Asynchronous: Message Brokers

RabbitMQ
// Publisher
        public class OrderEventPublisher : IOrderEventPublisher
        {
            private readonly IConnection _connection;
            private readonly IModel _channel;

            public OrderEventPublisher(IConnection connection)
            {
                _connection = connection;
                _channel = _connection.CreateModel();
                _channel.ExchangeDeclare("order-events", ExchangeType.Topic, durable: true);
            }

            public async Task PublishAsync<T>(T @event, string routingKey, CancellationToken ct = default)
            {
                var body = JsonSerializer.SerializeToUtf8Bytes(@event);
        
                var properties = _channel.CreateBasicProperties();
                properties.Persistent = true;
                properties.MessageId = Guid.NewGuid().ToString();
                properties.Timestamp = new AmqpTimestamp(DateTimeOffset.UtcNow.ToUnixTimeSeconds());
                properties.Type = typeof(T).Name;

                _channel.BasicPublish(
                    exchange: "order-events",
                    routingKey: routingKey,
                    basicProperties: properties,
                    body: body);
            }
        }

        // Consumer
        public class OrderEventHandler : BackgroundService
        {
            private readonly IConnection _connection;
            private readonly IServiceProvider _serviceProvider;
            private readonly ILogger<OrderEventHandler> _logger;

            public OrderEventHandler(
                IConnection connection,
                IServiceProvider serviceProvider,
                ILogger<OrderEventHandler> logger)
            {
                _connection = connection;
                _serviceProvider = serviceProvider;
                _logger = logger;
            }

            protected override Task ExecuteAsync(CancellationToken stoppingToken)
            {
                var channel = _connection.CreateModel();
                channel.ExchangeDeclare("order-events", ExchangeType.Topic, durable: true);
                channel.QueueDeclare("order-processing", durable: true, exclusive: false, autoDelete: false);
                channel.QueueBind("order-processing", "order-events", "order.*");

                var consumer = new AsyncEventingBasicConsumer(channel);
                consumer.Received += async (model, ea) =>
                {
                    using var scope = _serviceProvider.CreateScope();
                    var body = ea.Body.ToArray();
                    var @event = JsonSerializer.Deserialize<OrderCreatedEvent>(body);
            
                    try
                    {
                        var handler = scope.ServiceProvider.GetRequiredService<IOrderCreatedHandler>();
                        await handler.HandleAsync(@event, stoppingToken);
                        channel.BasicAck(ea.DeliveryTag, multiple: false);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Error processing event");
                        channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false); // DLQ
                    }
                };

                channel.BasicConsume("order-processing", autoAck: false, consumer: consumer);
        
                return Task.CompletedTask;
            }
        }
Kafka
// NuGet: Confluent.Kafka

        // Producer
        public class KafkaEventPublisher : IEventPublisher
        {
            private readonly IProducer<string, string> _producer;

            public KafkaEventPublisher(IProducer<string, string> producer) => _producer = producer;

            public async Task PublishAsync<T>(T @event, string topic, string key)
            {
                var message = new Message<string, string>
                {
                    Key = key,
                    Value = JsonSerializer.Serialize(@event)
                };

                await _producer.ProduceAsync(topic, message);
            }
        }

        // Consumer
        public class KafkaConsumer : BackgroundService
        {
            private readonly IConsumer<string, string> _consumer;
            private readonly IServiceProvider _serviceProvider;

            public KafkaConsumer(IConsumer<string, string> consumer, IServiceProvider serviceProvider)
            {
                _consumer = consumer;
                _serviceProvider = serviceProvider;
            }

            protected override Task ExecuteAsync(CancellationToken stoppingToken)
            {
                _consumer.Subscribe("order-events");

                while (!stoppingToken.IsCancellationRequested)
                {
                    var result = _consumer.Consume(stoppingToken);
            
                    using var scope = _serviceProvider.CreateScope();
                    var @event = JsonSerializer.Deserialize<OrderCreatedEvent>(result.Message.Value);
            
                    var handler = scope.ServiceProvider.GetRequiredService<IOrderCreatedHandler>();
                    await handler.HandleAsync(@event, stoppingToken);
                }

                return Task.CompletedTask;
            }
        }

Event-Driven Architecture

Event Sourcing Basics

State = f(Events)

        Order Created → Item Added → Item Added → Confirmed → Shipped
             │              │            │            │           │
             ▼              ▼            ▼            ▼           ▼
           Draft         Draft        Draft       Confirmed    Shipped

Принцип: Вместо хранения текущего состояния, храните все события, которые привели к этому состоянию.

CQRS (Command Query Responsibility Segregation)

┌──────────────────────────────────────────────────────┐
        │                    CQRS                               │
        │                                                       │
        │  ┌─────────────┐              ┌─────────────┐        │
        │  │   Commands  │              │   Queries   │        │
        │  │   (Write)   │              │   (Read)    │        │
        │  └──────┬──────┘              └──────┬──────┘        │
        │         │                            │               │
        │         ▼                            ▼               │
        │  ┌─────────────┐              ┌─────────────┐        │
        │  │ Write Model │              │ Read Model  │        │
        │  │  (Domain)   │              │ (Projections)│        │
        │  └──────┬──────┘              └──────┬──────┘        │
        │         │                            ▲               │
        │         │       Events               │               │
        │         └────────────────────────────┘               │
        └──────────────────────────────────────────────────────┘

Когда использовать CQRS:

  • Разные требования к read и write моделям
  • High read/write ratio
  • Разные команды работают с read и write
  • Event sourcing

Когда НЕ использовать:

  • Простые CRUD приложения
  • Нет scaling проблем
  • Команда маленькая

Distributed Transactions

Saga Pattern

Проблема: В микросервисах нет единой БД — нет ACID транзакций.

Решение: Saga — последовательность локальных транзакций с компенсирующими действиями.

Order Saga:
        1. Create Order (Order Service)
        2. Reserve Inventory (Inventory Service)
        3. Process Payment (Payment Service)
        4. Create Shipment (Shipping Service)

        Compensating Actions:
        1. Cancel Order
        2. Release Inventory
        3. Refund Payment
        4. Cancel Shipment
Choreography-based Saga
// Каждый сервис публикует события, другие реагируют
        public class OrderSagaChoreography
        {
            // Order Service
            public async Task CreateOrderAsync(CreateOrderCommand command)
            {
                var order = new Order(command.CustomerId);
                await _repository.SaveAsync(order);
        
                // Публикуем событие — другие сервисы реагируют
                await _eventPublisher.PublishAsync(new OrderCreatedEvent(order.Id));
            }
        }

        // Inventory Service
        public class InventorySagaHandler
        {
            public async Task HandleAsync(OrderCreatedEvent @event)
            {
                var reserved = await _inventory.ReserveAsync(@event.OrderId, @event.Items);
        
                if (reserved)
                    await _eventPublisher.PublishAsync(new InventoryReservedEvent(@event.OrderId));
                else
                    await _eventPublisher.PublishAsync(new InventoryReservationFailedEvent(@event.OrderId));
            }
        }

        // Payment Service
        public class PaymentSagaHandler
        {
            public async Task HandleAsync(InventoryReservedEvent @event)
            {
                var charged = await _payment.ChargeAsync(@event.OrderId);
        
                if (charged)
                    await _eventPublisher.PublishAsync(new PaymentCompletedEvent(@event.OrderId));
                else
                    await _eventPublisher.PublishAsync(new PaymentFailedEvent(@event.OrderId));
            }
        }
Orchestration-based Saga
public class OrderSagaOrchestrator : ISagaOrchestrator
        {
            private readonly IOrderService _orders;
            private readonly IInventoryService _inventory;
            private readonly IPaymentService _payment;
            private readonly IShippingService _shipping;
            private readonly ILogger<OrderSagaOrchestrator> _logger;

            public async Task ExecuteAsync(CreateOrderCommand command, CancellationToken ct = default)
            {
                var sagaData = new OrderSagaData
                {
                    OrderId = Guid.NewGuid(),
                    CustomerId = command.CustomerId,
                    Items = command.Items
                };

                try
                {
                    // Step 1: Create Order
                    sagaData.Order = await _orders.CreateAsync(sagaData.OrderId, sagaData.CustomerId, ct);
            
                    // Step 2: Reserve Inventory
                    await _inventory.ReserveAsync(sagaData.OrderId, sagaData.Items, ct);
            
                    // Step 3: Process Payment
                    await _payment.ChargeAsync(sagaData.OrderId, sagaData.Order.Total, ct);
            
                    // Step 4: Create Shipment
                    await _shipping.CreateShipmentAsync(sagaData.OrderId, ct);
            
                    // Step 5: Confirm Order
                    await _orders.ConfirmAsync(sagaData.OrderId, ct);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Saga failed, executing compensations");
                    await CompensateAsync(sagaData, ct);
                    throw;
                }
            }

            private async Task CompensateAsync(OrderSagaData sagaData, CancellationToken ct)
            {
                // Компенсации в обратном порядке
                await _shipping.CancelShipmentAsync(sagaData.OrderId, ct);
                await _payment.RefundAsync(sagaData.OrderId, ct);
                await _inventory.ReleaseAsync(sagaData.OrderId, ct);
                await _orders.CancelAsync(sagaData.OrderId, ct);
            }
        }

Outbox Pattern

Проблема: Как гарантировать, что сохранение в БД и публикация события — атомарны?

Решение: Сохраняем событие в ту же транзакцию, что и бизнес-данные. Отдельный процесс публикует события.

// Outbox Entity
        public class OutboxMessage
        {
            public Guid Id { get; set; }
            public string Type { get; set; }
            public string Content { get; set; }
            public DateTime OccurredOn { get; set; }
            public DateTime? ProcessedAt { get; set; }
            public int RetryCount { get; set; }
            public string Error { get; set; }
        }

        // В той же транзакции, что и бизнес-данные
        public class OrderRepository : IOrderRepository
        {
            private readonly AppDbContext _context;

            public async Task SaveAsync(Order order, CancellationToken ct = default)
            {
                _context.Orders.Update(order);
        
                // Сохраняем domain events в outbox — в той же транзакции!
                foreach (var @event in order.DomainEvents)
                {
                    _context.OutboxMessages.Add(new OutboxMessage
                    {
                        Id = Guid.NewGuid(),
                        Type = @event.GetType().Name,
                        Content = JsonSerializer.Serialize(@event),
                        OccurredOn = DateTime.UtcNow
                    });
                }
        
                // Один SaveChanges — атомарно!
                await _context.SaveChangesAsync(ct);
            }
        }

        // Background processor — публикует события
        public class OutboxProcessor : BackgroundService
        {
            private readonly IServiceProvider _serviceProvider;
            private readonly IEventPublisher _publisher;
            private readonly ILogger<OutboxProcessor> _logger;

            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    using var scope = _serviceProvider.CreateScope();
                    var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();

                    var messages = await context.OutboxMessages
                        .Where(m => m.ProcessedAt == null && m.RetryCount < 5)
                        .OrderBy(m => m.OccurredOn)
                        .Take(100)
                        .ToListAsync(stoppingToken);

                    foreach (var message in messages)
                    {
                        try
                        {
                            var @event = JsonSerializer.Deserialize(message.Content, Type.GetType(message.Type));
                            await _publisher.PublishAsync(@event, message.Type);
                    
                            message.ProcessedAt = DateTime.UtcNow;
                        }
                        catch (Exception ex)
                        {
                            message.RetryCount++;
                            message.Error = ex.Message;
                            _logger.LogError(ex, "Failed to process outbox message {Id}", message.Id);
                        }
                    }

                    await context.SaveChangesAsync(stoppingToken);
                    await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
                }
            }
        }

Практика

Задание 1: Outbox Pattern

Реализовать Outbox pattern для надёжной отправки событий:

  • Outbox messages сохраняются в той же транзакции
  • Background processor публикует события
  • Retry логика для failed messages
  • Dead letter queue для messages с max retries

Задание 2: API Gateway

Реализовать API Gateway с:

  • Rate limiting (fixed window, sliding window, token bucket)
  • Authentication (JWT validation)
  • Routing к разным микросервисам
  • Request/Response transformation

Задание 3: Saga Orchestrator

Реализовать saga orchestrator для distributed order processing:

  • Create Order → Reserve Inventory → Process Payment → Ship
  • Compensating actions для каждого шага
  • Saga persistence (сохранение состояния saga)
  • Timeout handling

Resilience Patterns

Polly Library

Overview

Polly — .NET library для transient fault handling. Реализует resilience patterns как first-class citizens.

NuGet: Polly
        GitHub: https://github.com/App-vNext/Polly

Retry Policy

// Базовый retry
        var retryPolicy = Policy
            .Handle<HttpRequestException>()
            .RetryAsync(3);

        // Retry с wait
        var retryWithWait = Policy
            .Handle<HttpRequestException>()
            .WaitAndRetryAsync(3, retryAttempt => 
                TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

        // Retry с callback
        var retryWithLogging = Policy
            .Handle<SqlException>(ex => ex.Number == 1205) // Deadlock
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retry => TimeSpan.FromSeconds(Math.Pow(2, retry)),
                onRetry: (exception, timespan, retryCount, context) =>
                {
                    context["Logger"].LogWarning(
                        "Retry {RetryCount} after {Delay}s due to {Exception}",
                        retryCount, timespan.TotalSeconds, exception.Message);
                });

        // Retry forever (с осторожностью!)
        var retryForever = Policy
            .Handle<Exception>()
            .RetryForeverAsync(onRetry: ex => Log.Error(ex, "Retrying..."));

        // Использование
        await retryWithLogging.ExecuteAsync(async ctx =>
        {
            ctx["Logger"] = logger;
            return await httpClient.GetAsync("https://api.example.com/data");
        }, new Context());

Circuit Breaker

// Circuit Breaker
        var circuitBreaker = Policy
            .Handle<HttpRequestException>()
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromSeconds(30),
                onBreak: (ex, duration) => 
                    Log.Warning($"Circuit broken for {duration}s due to {ex.Message}"),
                onReset: () => Log.Info("Circuit reset — healthy again"),
                onHalfOpen: () => Log.Info("Circuit half-open — testing"));

        // Advanced Circuit Breaker (на основе % failures)
        var advancedCircuitBreaker = Policy
            .Handle<HttpRequestException>()
            .AdvancedCircuitBreakerAsync(
                failureThreshold: 0.5,        // 50% failures
                samplingDuration: TimeSpan.FromSeconds(30),
                minimumThroughput: 10,         // Минимум 10 запросов за sampling
                durationOfBreak: TimeSpan.FromSeconds(60));

        // States: Closed → Open → Half-Open → Closed
        // Closed: нормальная работа
        // Open: все запросы fail fast
        // Half-Open: один test request, если success → Closed, если fail → Open

Bulkhead

// Bulkhead — ограничение параллелизма
        var bulkhead = Policy.BulkheadAsync(
            maxParallelization: 10,           // Максимум 10 параллельных запросов
            maxQueuingActions: 5,             // Ещё 5 в очереди
            onBulkheadRejected: async context =>
            {
                Log.Warning("Bulkhead full — request rejected");
                throw new BulkheadRejectedException("System at capacity");
            });

        // Isolated bulkheads для разных операций
        var bulkheadIsolation = Policy.BulkheadAsync(10, 5); // Для DB
        var bulkheadHttp = Policy.BulkheadAsync(20, 10);     // Для HTTP

        // Использование
        await bulkheadIsolation.ExecuteAsync(async () => 
            await dbContext.Orders.ToListAsync());

Timeout

// Optimistic Timeout (поддерживает CancellationToken)
        var timeout = Policy.TimeoutAsync(
            TimeSpan.FromSeconds(30),
            TimeoutStrategy.Optimistic,
            onTimeoutAsync: async (context, timespan, task) =>
            {
                Log.Warning("Operation timed out after {Timeout}s", timespan.TotalSeconds);
            });

        // Pessimistic Timeout (прерывает выполнение)
        var pessimisticTimeout = Policy.TimeoutAsync(
            TimeSpan.FromSeconds(30),
            TimeoutStrategy.Pessimistic);

        // Использование с HttpClient
        await timeout.ExecuteAsync(async ct =>
        {
            return await httpClient.GetAsync("https://api.example.com/data", ct);
        });

Fallback

// Fallback — значение по умолчанию при ошибке
        var fallback = Policy
            .Handle<HttpRequestException>()
            .FallbackAsync(async ct =>
            {
                Log.Warning("Using fallback value");
                return await GetCachedDataAsync();
            });

        // Fallback с fallback value
        var fallbackValue = Policy
            .Handle<Exception>()
            .Fallback<User>(
                fallbackValue: new User { Name = "Guest", IsAnonymous = true },
                onFallback: ex => Log.Warning(ex, "Falling back to anonymous user"));

        // Fallback с fallback action
        var fallbackAction = Policy
            .Handle<CacheException>()
            .FallbackAsync(
                fallbackAction: async ct => await GetFromDatabaseAsync(),
                onFallback: async (ex, ctx) => await Log.WarningAsync(ex, "Cache miss, using DB"));

Composite Policies — Policy Wrap

// Комбинирование политик
        var timeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(10));
        var retry = Policy
            .Handle<HttpRequestException>()
            .WaitAndRetryAsync(3, i => TimeSpan.FromSeconds(Math.Pow(2, i)));
        var circuitBreaker = Policy
            .Handle<HttpRequestException>()
            .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
        var fallback = Policy
            .Handle<Exception>()
            .FallbackAsync(async ct => await GetCachedResponseAsync());

        // Порядок: outer → inner
        // Fallback → CircuitBreaker → Retry → Timeout → Execution
        var resiliencePipeline = Policy
            .WrapAsync(fallback, circuitBreaker, retry, timeout);

        // Использование
        var response = await resiliencePipeline.ExecuteAsync(async ct =>
        {
            return await httpClient.GetAsync("https://api.example.com/data", ct);
        });

Polly v8 — Resilience Pipeline (новый API)

// NuGet: Microsoft.Extensions.Resilience (Polly v8)

        // Простой pipeline
        var pipeline = new ResiliencePipelineBuilder()
            .AddRetry(new RetryStrategyOptions
            {
                MaxRetryAttempts = 3,
                Delay = TimeSpan.FromSeconds(2),
                BackoffType = DelayBackoffType.Exponential,
                ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>()
            })
            .AddTimeout(TimeSpan.FromSeconds(10))
            .Build();

        // Pipeline с registry
        var pipelineRegistry = new ResiliencePipelineRegistry<string>();

        pipelineRegistry.TryAddPipeline("http-default", new ResiliencePipelineBuilder()
            .AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 3 })
            .AddCircuitBreaker(new CircuitBreakerStrategyOptions
            {
                FailureRatio = 0.5,
                SamplingDuration = TimeSpan.FromSeconds(30),
                MinimumThroughput = 10,
                BreakDuration = TimeSpan.FromSeconds(60)
            })
            .AddTimeout(TimeSpan.FromSeconds(30))
            .Build());

        // Использование
        var pipeline = pipelineRegistry.GetPipeline("http-default");
        await pipeline.ExecuteAsync(async ct => await httpClient.GetAsync(url, ct));

Resilient HTTP Client

Полная реализация

public class ResilientHttpClient
        {
            private readonly ResiliencePipeline _pipeline;
            private readonly HttpClient _httpClient;
            private readonly IMemoryCache _cache;
            private readonly ILogger<ResilientHttpClient> _logger;

            public ResilientHttpClient(
                HttpClient httpClient,
                IMemoryCache cache,
                ILogger<ResilientHttpClient> logger)
            {
                _httpClient = httpClient;
                _cache = cache;
                _logger = logger;

                _pipeline = new ResiliencePipelineBuilder()
                    .AddRetry(new RetryStrategyOptions
                    {
                        MaxRetryAttempts = 3,
                        Delay = TimeSpan.FromSeconds(1),
                        BackoffType = DelayBackoffType.Exponential,
                        ShouldHandle = new PredicateBuilder()
                            .Handle<HttpRequestException>()
                            .Handle<TimeoutRejectedException>()
                            .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
                    })
                    .AddCircuitBreaker(new CircuitBreakerStrategyOptions
                    {
                        FailureRatio = 0.5,
                        SamplingDuration = TimeSpan.FromSeconds(30),
                        MinimumThroughput = 10,
                        BreakDuration = TimeSpan.FromSeconds(60),
                        OnOpened = args =>
                        {
                            _logger.LogWarning("Circuit opened after {FailureCount} failures", 
                                args.FailureCount);
                            return default;
                        },
                        OnClosed = args =>
                        {
                            _logger.LogInformation("Circuit closed — service recovered");
                            return default;
                        }
                    })
                    .AddBulkhead(new BulkheadStrategyOptions
                    {
                        MaxParallelization = 20,
                        MaxQueuing = 10
                    })
                    .AddTimeout(TimeSpan.FromSeconds(30))
                    .Build();
            }

            public async Task<T> GetAsync<T>(string url, CancellationToken ct = default)
            {
                return await _pipeline.ExecuteAsync(async pipelineCt =>
                {
                    var response = await _httpClient.GetAsync(url, pipelineCt);
                    response.EnsureSuccessStatusCode();
                    return await response.Content.ReadFromJsonAsync<T>(pipelineCt);
                }, ct);
            }

            public async Task<T> GetWithCacheAsync<T>(string url, TimeSpan cacheDuration, CancellationToken ct = default)
            {
                // Проверяем кэш
                if (_cache.TryGetValue(url, out T cached))
                    return cached;

                // Запрос с resilience
                var result = await _pipeline.ExecuteAsync(async pipelineCt =>
                {
                    var response = await _httpClient.GetAsync(url, pipelineCt);
                    response.EnsureSuccessStatusCode();
                    return await response.Content.ReadFromJsonAsync<T>(pipelineCt);
                }, ct);

                // Сохраняем в кэш
                _cache.Set(url, result, cacheDuration);
                return result;
            }
        }

Adaptive Resilience

Dynamic Policy Adjustment

public class AdaptiveResiliencePolicy
        {
            private readonly ConcurrentQueue<RequestMetric> _metrics = new();
            private readonly TimeSpan _window = TimeSpan.FromMinutes(5);
            private ResiliencePipeline _currentPipeline;

            public AdaptiveResiliencePolicy()
            {
                _currentPipeline = CreateNormalPipeline();
            }

            public ResiliencePipeline GetCurrentPipeline()
            {
                CleanupOldMetrics();
                var errorRate = CalculateErrorRate();

                if (errorRate > 0.5)
                    return CreateDegradedPipeline();
                if (errorRate > 0.2)
                    return CreateCautiousPipeline();
        
                return _currentPipeline;
            }

            public void RecordMetric(RequestMetric metric)
            {
                _metrics.Enqueue(metric);
            }

            private ResiliencePipeline CreateNormalPipeline() => new ResiliencePipelineBuilder()
                .AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 3 })
                .AddTimeout(TimeSpan.FromSeconds(10))
                .Build();

            private ResiliencePipeline CreateCautiousPipeline() => new ResiliencePipelineBuilder()
                .AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 5, Delay = TimeSpan.FromSeconds(2) })
                .AddCircuitBreaker(new CircuitBreakerStrategyOptions { FailureRatio = 0.3, BreakDuration = TimeSpan.FromSeconds(30) })
                .AddTimeout(TimeSpan.FromSeconds(5))
                .Build();

            private ResiliencePipeline CreateDegradedPipeline() => new ResiliencePipelineBuilder()
                .AddCircuitBreaker(new CircuitBreakerStrategyOptions { FailureRatio = 0.1, BreakDuration = TimeSpan.FromSeconds(10) })
                .AddTimeout(TimeSpan.FromSeconds(2))
                .Build();

            private double CalculateErrorRate()
            {
                var recent = _metrics.Where(m => m.Timestamp > DateTime.UtcNow - _window).ToList();
                if (recent.Count == 0) return 0;
                return (double)recent.Count(m => m.IsError) / recent.Count;
            }

            private void CleanupOldMetrics()
            {
                var cutoff = DateTime.UtcNow - _window;
                while (_metrics.TryPeek(out var metric) && metric.Timestamp < cutoff)
                    _metrics.TryDequeue(out _);
            }
        }

        public record RequestMetric(DateTime Timestamp, bool IsError, TimeSpan Duration);

Chaos Engineering

Principles

  1. Build a hypothesis around steady state behavior — "System should respond in < 200ms"
  2. Vary real-world events — network latency, service crashes, disk full
  3. Run experiments in production — но с blast radius control
  4. Automate experiments — continuous chaos testing
  5. Minimize blast radius — canary, feature flags

Chaos Test Implementation

// Chaos Engineering middleware
        public class ChaosMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly ChaosConfig _config;
            private readonly ILogger<ChaosMiddleware> _logger;

            public ChaosMiddleware(RequestDelegate next, ChaosConfig config, ILogger<ChaosMiddleware> logger)
            {
                _next = next;
                _config = config;
                _logger = logger;
            }

            public async Task InvokeAsync(HttpContext context)
            {
                if (!_config.IsEnabled)
                {
                    await _next(context);
                    return;
                }

                // Random failures
                if (ShouldFail(_config.FailureRate))
                {
                    _logger.LogWarning("Chaos: Injecting failure for {Path}", context.Request.Path);
            
                    var failureType = GetRandomFailureType();
                    switch (failureType)
                    {
                        case FailureType.Exception:
                            throw new ChaosException("Chaos: Random failure");
                
                        case FailureType.Timeout:
                            await Task.Delay(_config.TimeoutDuration, context.RequestAborted);
                            break;
                
                        case FailureType.Latency:
                            await Task.Delay(_config.LatencyDuration, context.RequestAborted);
                            break;
                
                        case FailureType.StatusCode:
                            context.Response.StatusCode = _config.ErrorStatusCode;
                            return;
                    }
                }

                await _next(context);
            }

            private bool ShouldFail(double failureRate) => 
                Random.Shared.NextDouble() < failureRate;

            private FailureType GetRandomFailureType()
            {
                var types = Enum.GetValues<FailureType>();
                return types[Random.Shared.Next(types.Length)];
            }
        }

        public class ChaosConfig
        {
            public bool IsEnabled { get; set; }
            public double FailureRate { get; set; } = 0.01; // 1%
            public TimeSpan LatencyDuration { get; set; } = TimeSpan.FromSeconds(2);
            public TimeSpan TimeoutDuration { get; set; } = TimeSpan.FromSeconds(30);
            public int ErrorStatusCode { get; set; } = 500;
        }

        public enum FailureType { Exception, Timeout, Latency, StatusCode }
        public class ChaosException : Exception
        {
            public ChaosException(string message) : base(message) { }
        }

Chaos Testing с Polly

public class ChaosTest
        {
            private readonly ResiliencePipeline _pipeline;
            private readonly ChaosMiddleware _chaos;

            public ChaosTest(ResiliencePipeline pipeline)
            {
                _pipeline = pipeline;
            }

            public async Task RunChaosTestAsync(int iterations = 1000)
            {
                var successCount = 0;
                var failureCount = 0;
                var circuitBreakerCount = 0;

                for (int i = 0; i < iterations; i++)
                {
                    try
                    {
                        await _pipeline.ExecuteAsync(async ct =>
                        {
                            // Simulate external call with chaos
                            await SimulateExternalCallAsync(ct);
                        });
                        successCount++;
                    }
                    catch (BrokenCircuitException)
                    {
                        circuitBreakerCount++;
                    }
                    catch
                    {
                        failureCount++;
                    }
                }

                Console.WriteLine($"Success: {successCount}, Failed: {failureCount}, Circuit Breaker: {circuitBreakerCount}");
            }

            private async Task SimulateExternalCallAsync(CancellationToken ct)
            {
                // 10% chance of failure
                if (Random.Shared.NextDouble() < 0.1)
                    throw new HttpRequestException("Simulated failure");
        
                await Task.Delay(Random.Shared.Next(10, 500), ct);
            }
        }

Graceful Degradation vs Fail-Fast

Graceful Degradation

public class ProductService
        {
            private readonly IProductApiClient _api;
            private readonly IProductCache _cache;
            private readonly ResiliencePipeline _pipeline;

            public async Task<ProductDto> GetProductAsync(string id, CancellationToken ct = default)
            {
                try
                {
                    return await _pipeline.ExecuteAsync(async pipelineCt =>
                    {
                        return await _api.GetProductAsync(id, pipelineCt);
                    }, ct);
                }
                catch (Exception ex)
                {
                    // Graceful degradation: используем кэш
                    _logger.LogWarning(ex, "API failed, falling back to cache for product {Id}", id);
            
                    var cached = await _cache.GetAsync(id);
                    if (cached != null)
                    {
                        cached.IsStale = true;
                        return cached;
                    }

                    // Если кэша нет — возвращаем минимальную информацию
                    return new ProductDto
                    {
                        Id = id,
                        Name = "Product Unavailable",
                        IsAvailable = false,
                        FallbackMessage = "Product details temporarily unavailable"
                    };
                }
            }
        }

Fail-Fast

public class PaymentService
        {
            private readonly IPaymentGateway _gateway;
            private readonly ResiliencePipeline _pipeline;

            public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request, CancellationToken ct = default)
            {
                // Fail-fast: нет retry, нет fallback — платёж либо прошёл, либо нет
                var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(5));
        
                try
                {
                    return await timeoutPolicy.ExecuteAsync(async pipelineCt =>
                    {
                        return await _gateway.ChargeAsync(request, pipelineCt);
                    });
                }
                catch (TimeoutRejectedException)
                {
                    // Не знаем, прошёл платёж или нет — НЕЛЬЗЯ retry
                    throw new PaymentUncertainException(
                        "Payment status unknown — do not retry, check manually");
                }
                catch (Exception ex)
                {
                    throw new PaymentFailedException("Payment failed", ex);
                }
            }
        }

Stale-While-Revalidate

public class StaleWhileRevalidateCache<T>
        {
            private readonly IMemoryCache _cache;
            private readonly TimeSpan _stalenessThreshold;
            private readonly SemaphoreSlim _semaphore = new(1, 1);

            public StaleWhileRevalidateCache(IMemoryCache cache, TimeSpan stalenessThreshold)
            {
                _cache = cache;
                _stalenessThreshold = stalenessThreshold;
            }

            public async Task<T> GetOrRefreshAsync(
                string key,
                Func<Task<T>> factory,
                CancellationToken ct = default)
            {
                var cached = _cache.Get<CacheEntry<T>>(key);
        
                if (cached == null)
                {
                    // Нет кэша — загружаем
                    var value = await factory();
                    _cache.Set(key, new CacheEntry<T>(value, DateTime.UtcNow), 
                        new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });
                    return value;
                }

                // Есть кэш
                var age = DateTime.UtcNow - cached.StoredAt;
        
                if (age > _stalenessThreshold)
                {
                    // Stale — запускаем background refresh
                    _ = RefreshAsync(key, factory, ct);
                }

                return cached.Value;
            }

            private async Task RefreshAsync(string key, Func<Task<T>> factory, CancellationToken ct)
            {
                if (!await _semaphore.WaitAsync(TimeSpan.Zero, ct))
                    return; // Уже кто-то обновляет

                try
                {
                    var value = await factory();
                    _cache.Set(key, new CacheEntry<T>(value, DateTime.UtcNow),
                        new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });
                }
                finally
                {
                    _semaphore.Release();
                }
            }
        }

        public record CacheEntry<T>(T Value, DateTime StoredAt);

Практика

Задание 1: Resilient HTTP Client

Реализовать resilient HTTP client с composite Polly policies:

  • Retry с exponential backoff и jitter
  • Circuit Breaker с adaptive threshold
  • Bulkhead для isolation
  • Timeout с graceful degradation
  • Fallback с cached data

Задание 2: Chaos Test

Написать chaos test для проверки system behavior при failures:

  • Random latency injection
  • Random exception injection
  • Random timeout injection
  • Metrics collection и analysis
  • Pass/fail criteria

Задание 3: Fallback с Cached Data

Реализовать fallback mechanism с:

  • Stale-while-revalidate pattern
  • Cache invalidation при recovery
  • Monitoring staleness
  • Gradual degradation levels

Enterprise Architecture Patterns

Multi-Tenancy Patterns

Patterns Overview

PatternIsolationCostComplexityКогда использовать
Database per TenantМаксимальнаяВысокаяСредняяEnterprise, compliance, HIPAA
Schema per TenantВысокаяСредняяСредняяMid-market, PostgreSQL
Row-level (Discriminator)ЛогическаяНизкаяНизкаяSMB, SaaS с тысячами tenants

Database per Tenant

// Tenant resolution middleware
        public class TenantMiddleware
        {
            private readonly RequestDelegate _next;

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

            public async Task InvokeAsync(HttpContext context, ITenantResolver resolver, ITenantContext tenantContext)
            {
                var tenant = await resolver.ResolveAsync(context);
                if (tenant == null)
                {
                    context.Response.StatusCode = 401;
                    return;
                }

                tenantContext.SetTenant(tenant);
                await _next(context);
            }
        }

        // Tenant Context (Scoped)
        public class TenantContext : ITenantContext
        {
            public Tenant Tenant { get; private set; }

            public void SetTenant(Tenant tenant) => Tenant = tenant;
        }

        // Tenant-aware DbContext
        public class MultiTenantDbContext : DbContext
        {
            private readonly ITenantContext _tenantContext;

            public MultiTenantDbContext(DbContextOptions options, ITenantContext tenantContext) 
                : base(options)
            {
                _tenantContext = tenantContext;
            }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                // Выбираем connection string на основе tenant
                var connectionString = GetConnectionStringForTenant(_tenantContext.Tenant);
                optionsBuilder.UseSqlServer(connectionString);
            }

            private string GetConnectionStringForTenant(Tenant tenant)
            {
                // Из конфигурации или cache
                return $"Server=...;Database={tenant.DatabaseName};...";
            }
        }

        // Tenant-aware repository
        public class EfRepository<T> : IRepository<T> where T : class
        {
            private readonly MultiTenantDbContext _context;

            public EfRepository(MultiTenantDbContext context) => _context = context;

            public async Task<T> GetByIdAsync(Guid id, CancellationToken ct = default)
            {
                return await _context.Set<T>().FindAsync(new object[] { id }, ct);
            }
        }

Schema per Tenant

public class SchemaPerTenantDbContext : DbContext
        {
            private readonly ITenantContext _tenantContext;

            public SchemaPerTenantDbContext(DbContextOptions options, ITenantContext tenantContext)
                : base(options)
            {
                _tenantContext = tenantContext;
            }

            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                // Устанавливаем schema для всех entities
                var schema = _tenantContext.Tenant.SchemaName;
        
                foreach (var entity in modelBuilder.Model.GetEntityTypes())
                {
                    entity.SetSchema(schema);
                }
            }
        }

Row-level Multi-Tenancy

// TenantId на каждой entity
        public abstract class MultiTenantEntity
        {
            public Guid Id { get; set; }
            public Guid TenantId { get; set; }
        }

        // Global Query Filter — автоматически фильтрует по tenant
        public class AppDbContext : DbContext
        {
            private readonly ITenantContext _tenantContext;

            public AppDbContext(DbContextOptions options, ITenantContext tenantContext)
                : base(options)
            {
                _tenantContext = tenantContext;
            }

            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                // Применяем фильтр ко всем MultiTenantEntity
                foreach (var entityType in modelBuilder.Model.GetEntityTypes())
                {
                    if (typeof(MultiTenantEntity).IsAssignableFrom(entityType.ClrType))
                    {
                        var parameter = Expression.Parameter(entityType.ClrType, "e");
                        var property = Expression.Property(parameter, nameof(MultiTenantEntity.TenantId));
                        var tenantId = Expression.Constant(_tenantContext.Tenant.Id);
                        var equality = Expression.Equal(property, tenantId);
                        var lambda = Expression.Lambda(equality, parameter);
                
                        modelBuilder.Entity(entityType.ClrType).HasQueryFilter(lambda);
                    }
                }
            }
        }

        // Tenant resolution из subdomain
        public class SubdomainTenantResolver : ITenantResolver
        {
            private readonly ITenantStore _store;

            public async Task<Tenant> ResolveAsync(HttpContext context)
            {
                var host = context.Request.Host.Host; // tenant1.example.com
                var subdomain = host.Split('.')[0];
                return await _store.GetBySubdomainAsync(subdomain);
            }
        }

Plugin Architecture

Assembly Loading

public class PluginManager : IPluginManager
        {
            private readonly ILogger<PluginManager> _logger;
            private readonly List<IPlugin> _plugins = new();
            private readonly List<AssemblyLoadContext> _contexts = new();

            public async Task LoadPluginsAsync(string pluginsDirectory)
            {
                var pluginFiles = Directory.GetFiles(pluginsDirectory, "*.dll");

                foreach (var file in pluginFiles)
                {
                    try
                    {
                        var context = new PluginLoadContext(file);
                        var assembly = context.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(file)));
                
                        var pluginTypes = assembly.GetTypes()
                            .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract);

                        foreach (var type in pluginTypes)
                        {
                            var plugin = (IPlugin)Activator.CreateInstance(type);
                            await plugin.InitializeAsync();
                            _plugins.Add(plugin);
                            _contexts.Add(context);
                    
                            _logger.LogInformation("Loaded plugin: {PluginName}", plugin.Name);
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Failed to load plugin from {File}", file);
                    }
                }
            }

            public IEnumerable<IPlugin> GetPlugins<T>() where T : IPlugin =>
                _plugins.OfType<T>();

            public async Task UnloadPluginAsync(string pluginName)
            {
                var plugin = _plugins.FirstOrDefault(p => p.Name == pluginName);
                if (plugin == null) return;

                await plugin.DisposeAsync();
                _plugins.Remove(plugin);

                var context = _contexts.FirstOrDefault();
                context?.Unload();
                _contexts.Remove(context);
            }
        }

        // Isolated AssemblyLoadContext для hot-reload
        public class PluginLoadContext : AssemblyLoadContext
        {
            private readonly AssemblyDependencyResolver _resolver;

            public PluginLoadContext(string pluginPath) : base(isCollectible: true)
            {
                _resolver = new AssemblyDependencyResolver(pluginPath);
            }

            protected override Assembly Load(AssemblyName assemblyName)
            {
                var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
                if (assemblyPath != null)
                {
                    return LoadFromAssemblyPath(assemblyPath);
                }
                return null;
            }

            protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
            {
                var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
                if (!string.IsNullOrEmpty(libraryPath))
                {
                    return LoadUnmanagedDllFromPath(libraryPath);
                }
                return IntPtr.Zero;
            }
        }

        // Plugin interface
        public interface IPlugin : IAsyncDisposable
        {
            string Name { get; }
            string Version { get; }
            Task InitializeAsync();
        }

        // Plugin implementation
        public class PaymentPlugin : IPlugin
        {
            public string Name => "StripePayment";
            public string Version => "1.0.0";

            public Task InitializeAsync()
            {
                // Регистрация в DI, настройка
                return Task.CompletedTask;
            }

            public ValueTask DisposeAsync()
            {
                // Cleanup
                return ValueTask.CompletedTask;
            }
        }

Hot-Reload

public class HotReloadPluginManager : BackgroundService
        {
            private readonly PluginManager _pluginManager;
            private readonly string _pluginsDirectory;
            private readonly ILogger<HotReloadPluginManager> _logger;
            private FileSystemWatcher _watcher;

            public HotReloadPluginManager(
                PluginManager pluginManager,
                string pluginsDirectory,
                ILogger<HotReloadPluginManager> logger)
            {
                _pluginManager = pluginManager;
                _pluginsDirectory = pluginsDirectory;
                _logger = logger;
            }

            protected override Task ExecuteAsync(CancellationToken stoppingToken)
            {
                _watcher = new FileSystemWatcher(_pluginsDirectory, "*.dll");
                _watcher.Created += async (s, e) => await OnPluginAddedAsync(e.FullPath);
                _watcher.Deleted += async (s, e) => await OnPluginRemovedAsync(e.FullPath);
                _watcher.Changed += async (s, e) => await OnPluginChangedAsync(e.FullPath);
                _watcher.EnableRaisingEvents = true;

                return Task.CompletedTask;
            }

            private async Task OnPluginChangedAsync(string path)
            {
                var pluginName = Path.GetFileNameWithoutExtension(path);
                _logger.LogInformation("Hot-reloading plugin: {PluginName}", pluginName);

                await _pluginManager.UnloadPluginAsync(pluginName);
        
                // Ждём, пока файл освободится
                await Task.Delay(1000);
        
                await _pluginManager.LoadPluginsAsync(_pluginsDirectory);
            }
        }

Feature Toggles

Launch Darkly Pattern

// Feature Flag интерфейс
        public interface IFeatureManager
        {
            Task<bool> IsEnabledAsync(string featureName, CancellationToken ct = default);
            Task<bool> IsEnabledAsync(string featureName, User user, CancellationToken ct = default);
            Task<double> GetRolloutPercentageAsync(string featureName, CancellationToken ct = default);
        }

        // Feature Flag сервис
        public class FeatureManager : IFeatureManager
        {
            private readonly IFeatureStore _store;
            private readonly IMemoryCache _cache;

            public async Task<bool> IsEnabledAsync(string featureName, User user, CancellationToken ct = default)
            {
                var flag = await _store.GetAsync(featureName);
                if (flag == null) return false;
                if (!flag.Enabled) return false;

                // Gradual rollout
                if (flag.RolloutPercentage < 100)
                {
                    var hash = GetConsistentHash(featureName, user.Id);
                    if (hash > flag.RolloutPercentage) return false;
                }

                // Targeting rules
                if (flag.TargetingRules.Any())
                {
                    foreach (var rule in flag.TargetingRules)
                    {
                        if (rule.Matches(user))
                            return rule.Value;
                    }
                }

                return flag.DefaultValue;
            }

            private int GetConsistentHash(string featureName, string userId)
            {
                using var sha256 = SHA256.Create();
                var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes($"{featureName}:{userId}"));
                return Math.Abs(BitConverter.ToInt32(bytes, 0)) % 100;
            }
        }

        // Feature Flag конфигурация
        public class FeatureFlag
        {
            public string Name { get; set; }
            public bool Enabled { get; set; }
            public bool DefaultValue { get; set; }
            public double RolloutPercentage { get; set; } = 100;
            public List<TargetingRule> TargetingRules { get; set; } = new();
        }

        public class TargetingRule
        {
            public Expression<Func<User, bool>> Condition { get; set; }
            public bool Value { get; set; }

            public bool Matches(User user) => Condition.Compile()(user);
        }

        // Использование
        public class CheckoutController : ControllerBase
        {
            private readonly IFeatureManager _features;

            [HttpPost("checkout")]
            public async Task<ActionResult> Checkout(CheckoutRequest request)
            {
                var user = GetCurrentUser();
        
                if (await _features.IsEnabledAsync("new-checkout-flow", user))
                {
                    return await ProcessNewCheckoutAsync(request);
                }
        
                return await ProcessLegacyCheckoutAsync(request);
            }
        }

        // Feature Flag middleware для kill switch
        public class FeatureFlagMiddleware
        {
            private readonly RequestDelegate _next;

            public async Task InvokeAsync(HttpContext context, IFeatureManager features)
            {
                var path = context.Request.Path.Value;
        
                if (path.StartsWith("/api/v2/") && !await features.IsEnabledAsync("api-v2"))
                {
                    context.Response.StatusCode = 404;
                    return;
                }

                await _next(context);
            }
        }

Blue-Green Deployment

Концепция

┌──────────────────────────────────────────────────┐
        │                  Load Balancer                    │
        │                                                   │
        │  ┌─────────────┐          ┌─────────────┐        │
        │  │   Blue      │          │   Green     │        │
        │  │  (active)   │          │  (idle)     │        │
        │  │  v2.0       │          │  v2.1       │        │
        │  └─────────────┘          └─────────────┘        │
        └──────────────────────────────────────────────────┘

        Switch: Load Balancer переключает traffic на Green
        Rollback: Load Balancer переключает traffic обратно на Blue

Реализация в .NET

// Health check для readiness
        public class DeploymentHealthCheck : IHealthCheck
        {
            private readonly IDeploymentState _deploymentState;

            public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken ct = default)
            {
                if (_deploymentState.IsReady)
                    return Task.FromResult(HealthCheckResult.Healthy("Deployment is ready"));
        
                return Task.FromResult(HealthCheckResult.Unhealthy("Deployment is not ready"));
            }
        }

        // Readiness probe
        app.MapHealthChecks("/health/ready", new HealthCheckOptions
        {
            Predicate = check => check.Name == "deployment"
        });

        // Liveness probe
        app.MapHealthChecks("/health/live", new HealthCheckOptions
        {
            Predicate = check => check.Name == "liveness"
        });

Canary Releases

┌─────────────────────────────────────────────────────┐
        │                  Traffic Router                       │
        │                                                       │
        │  95% ──────────────▶ Blue (v2.0)                     │
        │                                                       │
        │  5%  ──────────────▶ Green (v2.1) ← Canary           │
        │                                                       │
        │  Если metrics OK → увеличиваем % canary              │
        │  Если metrics BAD → rollback                         │
        └─────────────────────────────────────────────────────┘
public class CanaryMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly IFeatureManager _features;
            private readonly IMetricsService _metrics;

            public async Task InvokeAsync(HttpContext context)
            {
                var canaryPercentage = await _features.GetRolloutPercentageAsync("canary-release");
        
                if (IsCanaryUser(context, canaryPercentage))
                {
                    // Canary user — направляем на новую версию
                    context.Items["Version"] = "canary";
                    context.Response.Headers["X-Version"] = "canary";
                }
                else
                {
                    context.Items["Version"] = "stable";
                    context.Response.Headers["X-Version"] = "stable";
                }

                await _next(context);
            }

            private bool IsCanaryUser(HttpContext context, double percentage)
            {
                var userId = context.User.FindFirst("sub")?.Value;
                if (userId == null) return false;

                var hash = GetConsistentHash("canary", userId);
                return hash < percentage;
            }
        }

Strangler Fig Pattern

Концепция

Постепенная миграция legacy системы путём "оборачивания" старой функциональности новой.

┌─────────────────────────────────────────────────────┐
        │                  Facade / Proxy                       │
        │                                                       │
        │  Request ──▶ ┌───────────────┐                       │
        │              │   Router       │                       │
        │              │                │                       │
        │              │  /api/orders ──▶ New Service           │
        │              │  /api/users  ──▶ New Service           │
        │              │  /api/legacy ──▶ Legacy System         │
        │              └───────────────┘                       │
        └─────────────────────────────────────────────────────┘

        Шаг 1: Создаём Facade перед legacy системой
        Шаг 2: По одному переносим endpoints в новые сервисы
        Шаг 3: Когда всё перенесено — убираем legacy

Реализация

public class StranglerMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly HttpClient _legacyClient;
            private readonly IFeatureManager _features;

            public async Task InvokeAsync(HttpContext context)
            {
                var path = context.Request.Path.Value;

                // Проверяем, мигрирован ли endpoint
                if (await _features.IsEnabledAsync($"migrated-{path}"))
                {
                    // Новый сервис обрабатывает
                    await _next(context);
                }
                else
                {
                    // Proxy к legacy системе
                    await ProxyToLegacyAsync(context);
                }
            }

            private async Task ProxyToLegacyAsync(HttpContext context)
            {
                var request = new HttpRequestMessage(
                    new HttpMethod(context.Request.Method),
                    $"https://legacy-system{context.Request.Path}{context.Request.QueryString}");

                // Копируем headers и body
                foreach (var header in context.Request.Headers)
                    request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());

                if (context.Request.Method != "GET" && context.Request.Method != "HEAD")
                    request.Content = new StreamContent(context.Request.Body);

                var response = await _legacyClient.SendAsync(request);

                context.Response.StatusCode = (int)response.StatusCode;
                foreach (var header in response.Headers)
                    context.Response.Headers[header.Key] = header.Value.ToArray();

                await response.Content.CopyToAsync(context.Response.Body);
            }
        }

Практика

Задание 1: Multi-Tenant Middleware

Реализовать multi-tenant middleware с:

  • Tenant resolution из subdomain / header / JWT claim
  • Tenant isolation (database per tenant или row-level)
  • Tenant-aware DbContext с global query filters
  • Tenant-specific configuration

Задание 2: Plugin System с Hot-Reload

Создать plugin system с:

  • Assembly loading через AssemblyLoadContext
  • Plugin isolation (каждый plugin в отдельном context)
  • Hot-reload при изменении DLL
  • Plugin lifecycle (initialize, dispose)

Задание 3: Feature Flag System

Реализовать feature flag system с:

  • Gradual rollout (percentage-based)
  • User targeting rules
  • Kill switch
  • Dynamic configuration (без restart)
  • Audit log изменений

Event Sourcing и CQRS

Event Store

Append-Only Stream

// Event — неизменяемый факт
        public abstract class DomainEvent
        {
            public Guid EventId { get; } = Guid.NewGuid();
            public DateTime OccurredAt { get; } = DateTime.UtcNow;
            public int Version { get; set; }
        }

        // Конкретные события
        public record OrderCreatedEvent(Guid OrderId, Guid CustomerId, decimal Total) : DomainEvent;
        public record OrderItemAddedEvent(Guid OrderId, Guid ProductId, int Quantity, decimal Price) : DomainEvent;
        public record OrderItemRemovedEvent(Guid OrderId, Guid ProductId) : DomainEvent;
        public record OrderConfirmedEvent(Guid OrderId) : DomainEvent;
        public record OrderCancelledEvent(Guid OrderId, string Reason) : DomainEvent;

        // Event Stream — последовательность событий для одного Aggregate
        public class EventStream
        {
            public string StreamId { get; }
            public int CurrentVersion { get; private set; }
            private readonly List<DomainEvent> _events = new();

            public EventStream(string streamId, int currentVersion = 0)
            {
                StreamId = streamId;
                CurrentVersion = currentVersion;
            }

            public IReadOnlyList<DomainEvent> Events => _events.AsReadOnly();

            public void AddEvent(DomainEvent @event)
            {
                CurrentVersion++;
                @event.Version = CurrentVersion;
                _events.Add(@event);
            }
        }

        // Event Store — append-only
        public interface IEventStore
        {
            Task<EventStream> LoadStreamAsync(string streamId, CancellationToken ct = default);
            Task SaveStreamAsync(EventStream stream, int expectedVersion, CancellationToken ct = default);
        }

        public class SqlEventStore : IEventStore
        {
            private readonly IDbConnection _connection;
            private readonly ISerializer _serializer;

            public async Task<EventStream> LoadStreamAsync(string streamId, CancellationToken ct = default)
            {
                var sql = """
                    SELECT Version, EventType, EventData, OccurredAt 
                    FROM Events 
                    WHERE StreamId = @StreamId 
                    ORDER BY Version
                    """;

                var events = new List<DomainEvent>();
                int version = 0;

                var results = await _connection.QueryAsync<EventRecord>(sql, new { StreamId = streamId });
        
                foreach (var record in results)
                {
                    version = record.Version;
                    var @event = _serializer.Deserialize(record.EventData, record.EventType);
                    @event.Version = record.Version;
                    events.Add(@event);
                }

                return new EventStream(streamId, version) { Events = events };
            }

            public async Task SaveStreamAsync(EventStream stream, int expectedVersion, CancellationToken ct = default)
            {
                using var transaction = _connection.BeginTransaction();
        
                try
                {
                    // Optimistic concurrency check
                    var currentVersion = await _connection.ExecuteScalarAsync<int>(
                        "SELECT COALESCE(MAX(Version), 0) FROM Events WHERE StreamId = @StreamId",
                        new { StreamId = stream.StreamId },
                        transaction);

                    if (currentVersion != expectedVersion)
                        throw new ConcurrencyException(
                            $"Stream {stream.StreamId} expected version {expectedVersion} but found {currentVersion}");

                    // Append events
                    foreach (var @event in stream.Events)
                    {
                        var sql = """
                            INSERT INTO Events (StreamId, Version, EventType, EventData, OccurredAt)
                            VALUES (@StreamId, @Version, @EventType, @EventData, @OccurredAt)
                            """;

                        await _connection.ExecuteAsync(sql, new
                        {
                            StreamId = stream.StreamId,
                            Version = @event.Version,
                            EventType = @event.GetType().Name,
                            EventData = _serializer.Serialize(@event),
                            OccurredAt = @event.OccurredAt
                        }, transaction);
                    }

                    transaction.Commit();
                }
                catch
                {
                    transaction.Rollback();
                    throw;
                }
            }
        }

        public record EventRecord(int Version, string EventType, string EventData, DateTime OccurredAt);

Aggregate из Events

// Aggregate с Event Sourcing
        public class Order : AggregateRoot
        {
            public Guid Id { get; private set; }
            public Guid CustomerId { get; private set; }
            public OrderStatus Status { get; private set; }
            public decimal Total { get; private set; }
            private readonly List<OrderItem> _items = new();

            public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();

            // Static factory — создаёт новый aggregate
            public static Order Create(Guid customerId)
            {
                var order = new Order();
                var @event = new OrderCreatedEvent(Guid.NewGuid(), customerId, 0);
                order.Apply(@event);
                order.AddUncommittedEvent(@event);
                return order;
            }

            // Методы, которые генерируют события
            public void AddItem(Guid productId, int quantity, decimal price)
            {
                if (Status != OrderStatus.Draft)
                    throw new DomainException("Cannot modify non-draft order");

                var @event = new OrderItemAddedEvent(Id, productId, quantity, price);
                Apply(@event);
                AddUncommittedEvent(@event);
            }

            public void RemoveItem(Guid productId)
            {
                if (Status != OrderStatus.Draft)
                    throw new DomainException("Cannot modify non-draft order");

                var @event = new OrderItemRemovedEvent(Id, productId);
                Apply(@event);
                AddUncommittedEvent(@event);
            }

            public void Confirm()
            {
                if (Status != OrderStatus.Draft)
                    throw new DomainException("Order must be in Draft status");

                if (!_items.Any())
                    throw new DomainException("Cannot confirm empty order");

                var @event = new OrderConfirmedEvent(Id);
                Apply(@event);
                AddUncommittedEvent(@event);
            }

            public void Cancel(string reason)
            {
                if (Status == OrderStatus.Cancelled)
                    throw new DomainException("Order already cancelled");

                var @event = new OrderCancelledEvent(Id, reason);
                Apply(@event);
                AddUncommittedEvent(@event);
            }

            // Apply — изменяет state на основе события
            private void Apply(OrderCreatedEvent @event)
            {
                Id = @event.OrderId;
                CustomerId = @event.CustomerId;
                Status = OrderStatus.Draft;
                Total = 0;
            }

            private void Apply(OrderItemAddedEvent @event)
            {
                _items.Add(new OrderItem(@event.ProductId, @event.Quantity, @event.Price));
                Total += @event.Quantity * @event.Price;
            }

            private void Apply(OrderItemRemovedEvent @event)
            {
                var item = _items.FirstOrDefault(i => i.ProductId == @event.ProductId);
                if (item != null)
                {
                    Total -= item.Quantity * item.Price;
                    _items.Remove(item);
                }
            }

            private void Apply(OrderConfirmedEvent @event) => Status = OrderStatus.Confirmed;
            private void Apply(OrderCancelledEvent @event) => Status = OrderStatus.Cancelled;

            // Rebuild state из событий
            public void RebuildFromHistory(IEnumerable<DomainEvent> history)
            {
                foreach (var @event in history)
                {
                    Apply(@event);
                }
                ClearUncommittedEvents();
            }
        }

        // Базовый Aggregate Root для ES
        public abstract class AggregateRoot
        {
            private readonly List<DomainEvent> _uncommittedEvents = new();

            public IReadOnlyList<DomainEvent> UncommittedEvents => _uncommittedEvents.AsReadOnly();

            protected void AddUncommittedEvent(DomainEvent @event) => _uncommittedEvents.Add(@event);
    
            protected void ClearUncommittedEvents() => _uncommittedEvents.Clear();

            public void MarkEventsAsCommitted() => _uncommittedEvents.Clear();
        }

Projections — Read Model Building

Projection Handler

// Projection — строит read model из events
        public interface IProjection
        {
            string Name { get; }
            Task HandleAsync(DomainEvent @event, CancellationToken ct = default);
        }

        // Order Summary Projection
        public class OrderSummaryProjection : IProjection
        {
            public string Name => "OrderSummary";
            private readonly IDbConnection _connection;

            public async Task HandleAsync(DomainEvent @event, CancellationToken ct = default)
            {
                switch (@event)
                {
                    case OrderCreatedEvent e:
                        await _connection.ExecuteAsync("""
                            INSERT INTO OrderSummary (OrderId, CustomerId, Total, Status, CreatedAt)
                            VALUES (@OrderId, @CustomerId, @Total, @Status, @CreatedAt)
                            """, new
                            {
                                OrderId = e.OrderId,
                                CustomerId = e.CustomerId,
                                Total = e.Total,
                                Status = "Draft",
                                CreatedAt = e.OccurredAt
                            });
                        break;

                    case OrderItemAddedEvent e:
                        await _connection.ExecuteAsync("""
                            UPDATE OrderSummary 
                            SET Total = Total + (@Quantity * @Price), ItemCount = ItemCount + 1
                            WHERE OrderId = @OrderId
                            """, new { e.OrderId, e.Quantity, e.Price });
                        break;

                    case OrderConfirmedEvent e:
                        await _connection.ExecuteAsync("""
                            UPDATE OrderSummary SET Status = 'Confirmed' WHERE OrderId = @OrderId
                            """, new { e.OrderId });
                        break;

                    case OrderCancelledEvent e:
                        await _connection.ExecuteAsync("""
                            UPDATE OrderSummary SET Status = 'Cancelled' WHERE OrderId = @OrderId
                            """, new { e.OrderId });
                        break;
                }
            }
        }

        // Event Processor — dispatches events to projections
        public class EventProcessor : BackgroundService
        {
            private readonly IEventStore _eventStore;
            private readonly IEnumerable<IProjection> _projections;
            private readonly IPositionTracker _positionTracker;
            private readonly ILogger<EventProcessor> _logger;

            public EventProcessor(
                IEventStore eventStore,
                IEnumerable<IProjection> projections,
                IPositionTracker positionTracker,
                ILogger<EventProcessor> logger)
            {
                _eventStore = eventStore;
                _projections = projections;
                _positionTracker = positionTracker;
                _logger = logger;
            }

            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    var lastPosition = await _positionTracker.GetLastPositionAsync();
            
                    var newEvents = await _eventStore.GetEventsAfterAsync(lastPosition, stoppingToken);

                    foreach (var @event in newEvents)
                    {
                        foreach (var projection in _projections)
                        {
                            try
                            {
                                await projection.HandleAsync(@event, stoppingToken);
                            }
                            catch (Exception ex)
                            {
                                _logger.LogError(ex, "Projection {Projection} failed on event {Event}",
                                    projection.Name, @event.GetType().Name);
                            }
                        }

                        await _positionTracker.UpdatePositionAsync(@event.Version);
                    }

                    await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
                }
            }
        }

Snapshotting

Зачем нужны Snapshots

Проблема: При большом количестве событий rebuild aggregate становится медленным.

Решение: Периодически сохраняем snapshot состояния aggregate.

Events:  [E1, E2, E3, ..., E99, E100]
        Snapshot:           [State@50]

        Rebuild: Load Snapshot@50 → Apply E51..E100 (вместо E1..E100)

Реализация

public class Snapshot
        {
            public string StreamId { get; set; }
            public int Version { get; set; }
            public string State { get; set; }  // Сериализованное состояние
            public DateTime CreatedAt { get; set; }
        }

        public interface ISnapshotStore
        {
            Task<Snapshot> GetSnapshotAsync(string streamId, CancellationToken ct = default);
            Task SaveSnapshotAsync(Snapshot snapshot, CancellationToken ct = default);
        }

        public class AggregateWithSnapshotting
        {
            private readonly IEventStore _eventStore;
            private readonly ISnapshotStore _snapshotStore;
            private readonly int _snapshotThreshold = 50;

            public async Task<T> LoadAggregateAsync<T>(string streamId, CancellationToken ct = default)
                where T : AggregateRoot, new()
            {
                // 1. Пробуем загрузить snapshot
                var snapshot = await _snapshotStore.GetSnapshotAsync(streamId, ct);
        
                var aggregate = new T();
                int fromVersion = 0;

                if (snapshot != null)
                {
                    // Восстанавливаем из snapshot
                    aggregate = DeserializeSnapshot<T>(snapshot);
                    fromVersion = snapshot.Version;
                }

                // 2. Загружаем события после snapshot
                var events = await _eventStore.GetEventsAfterAsync(streamId, fromVersion, ct);
        
                foreach (var @event in events)
                {
                    aggregate.ApplyEvent(@event);
                }

                return aggregate;
            }

            public async Task SaveAggregateAsync(AggregateRoot aggregate, int expectedVersion, CancellationToken ct = default)
            {
                await _eventStore.SaveStreamAsync(aggregate, expectedVersion, ct);

                // 3. Сохраняем snapshot если threshold достигнут
                if (aggregate.Version >= _snapshotThreshold && 
                    aggregate.Version % _snapshotThreshold == 0)
                {
                    var snapshot = new Snapshot
                    {
                        StreamId = aggregate.StreamId,
                        Version = aggregate.Version,
                        State = SerializeAggregate(aggregate),
                        CreatedAt = DateTime.UtcNow
                    };

                    await _snapshotStore.SaveSnapshotAsync(snapshot, ct);
                }
            }
        }

Event Versioning и Migration

Проблема

v1: OrderCreatedEvent { OrderId, CustomerId, Total }
        v2: OrderCreatedEvent { OrderId, CustomerId, Total, Currency }
        v3: OrderCreatedEvent { OrderId, CustomerId, Items, Total, Currency }

Upcasting

public interface IEventUpcaster
        {
            DomainEvent Upcast(DomainEvent @event);
            bool CanUpcast(DomainEvent @event);
        }

        // Upcaster v1 → v2
        public class OrderCreatedEventV1ToV2Upcaster : IEventUpcaster
        {
            public bool CanUpcast(DomainEvent @event) =>
                @event is OrderCreatedEventV1;

            public DomainEvent Upcast(DomainEvent @event)
            {
                var v1 = (OrderCreatedEventV1)@event;
                return new OrderCreatedEventV2(
                    v1.OrderId,
                    v1.CustomerId,
                    v1.Total,
                    "USD" // Default для старых событий
                );
            }
        }

        // Upcaster v2 → v3
        public class OrderCreatedEventV2ToV3Upcaster : IEventUpcaster
        {
            public bool CanUpcast(DomainEvent @event) =>
                @event is OrderCreatedEventV2;

            public DomainEvent Upcast(DomainEvent @event)
            {
                var v2 = (OrderCreatedEventV2)@event;
                return new OrderCreatedEventV3(
                    v2.OrderId,
                    v2.CustomerId,
                    new[] { new OrderItemSnapshot { Total = v2.Total } }, // Approximation
                    v2.Total,
                    v2.Currency
                );
            }
        }

        // Upcaster pipeline
        public class EventUpcasterPipeline
        {
            private readonly List<IEventUpcaster> _upcasters = new();

            public void AddUpcaster(IEventUpcaster upcaster) => _upcasters.Add(upcaster);

            public DomainEvent Upcast(DomainEvent @event)
            {
                var current = @event;
        
                while (true)
                {
                    var upcaster = _upcasters.FirstOrDefault(u => u.CanUpcast(current));
                    if (upcaster == null) break;
            
                    current = upcaster.Upcast(current);
                }

                return current;
            }
        }

        // Использование при загрузке событий
        public class EventStoreWithUpcasting : IEventStore
        {
            private readonly IEventStore _inner;
            private readonly EventUpcasterPipeline _upcasters;

            public async Task<EventStream> LoadStreamAsync(string streamId, CancellationToken ct = default)
            {
                var stream = await _inner.LoadStreamAsync(streamId, ct);
        
                var upcastedEvents = stream.Events.Select(e => _upcasters.Upcast(e)).ToList();
        
                return new EventStream(streamId, stream.CurrentVersion)
                {
                    Events = upcastedEvents
                };
            }
        }

Event Schema Evolution с JSON

// Используем JSON с $schema полем
        public record OrderCreatedEventV1(
            Guid OrderId,
            Guid CustomerId,
            decimal Total,
            string SchemaVersion = "1.0");

        public record OrderCreatedEventV2(
            Guid OrderId,
            Guid CustomerId,
            decimal Total,
            string Currency,
            string SchemaVersion = "2.0");

        // При десериализации проверяем версию
        public class VersionedEventSerializer : ISerializer
        {
            public DomainEvent Deserialize(string json, string eventType)
            {
                using var doc = JsonDocument.Parse(json);
                var version = doc.RootElement.GetProperty("SchemaVersion").GetString();

                return version switch
                {
                    "1.0" => JsonSerializer.Deserialize<OrderCreatedEventV1>(json),
                    "2.0" => JsonSerializer.Deserialize<OrderCreatedEventV2>(json),
                    _ => throw new UnknownSchemaVersionException(version)
                };
            }
        }

CQRS Trade-offs

Когда CQRS полезен

СценарийБез CQRSС CQRS
Simple CRUD✅ Просто❌ Overkill
Read-heavy (100:1)❌ Медленные reads✅ Оптимизированные read models
Complex queries❌ Domain model polluted✅ Специализированные projections
Event Sourcing❌ Невозможно без CQRS✅ Естественное разделение
Team scaling❌ Конфликты на модели✅ Разные команды на read/write

CQRS с MediatR

// Commands (Write)
        public record CreateOrderCommand(Guid CustomerId, List<OrderItemDto> Items) : IRequest<OrderResult>;

        public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, OrderResult>
        {
            private readonly IEventStore _eventStore;

            public async Task<OrderResult> Handle(CreateOrderCommand request, CancellationToken ct)
            {
                var order = Order.Create(request.CustomerId);
        
                foreach (var item in request.Items)
                    order.AddItem(item.ProductId, item.Quantity, item.Price);

                await _eventStore.SaveStreamAsync(order, -1, ct);
        
                return new OrderResult(order.Id, order.Status);
            }
        }

        // Queries (Read)
        public record GetOrderQuery(Guid OrderId) : IRequest<OrderDto>;

        public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto>
        {
            private readonly IReadDbContext _readDb;

            public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct)
            {
                // Читаем из оптимизированной read model
                return await _readDb.Orders
                    .Where(o => o.Id == request.OrderId)
                    .Select(o => new OrderDto
                    {
                        Id = o.Id,
                        CustomerId = o.CustomerId,
                        Total = o.Total,
                        Status = o.Status,
                        Items = o.Items.Select(i => new OrderItemDto
                        {
                            ProductId = i.ProductId,
                            Quantity = i.Quantity,
                            Price = i.Price
                        }).ToList()
                    })
                    .FirstOrDefaultAsync(ct);
            }
        }

        // Controller
        [ApiController]
        [Route("api/orders")]
        public class OrdersController : ControllerBase
        {
            private readonly IMediator _mediator;

            public OrdersController(IMediator mediator) => _mediator = mediator;

            [HttpPost]
            public async Task<ActionResult<OrderResult>> CreateOrder(CreateOrderCommand command)
            {
                var result = await _mediator.Send(command);
                return CreatedAtAction(nameof(GetOrder), new { id = result.Id }, result);
            }

            [HttpGet("{id}")]
            public async Task<ActionResult<OrderDto>> GetOrder(Guid id)
            {
                var order = await _mediator.Send(new GetOrderQuery(id));
                if (order == null) return NotFound();
                return order;
            }
        }

Практика

Задание 1: Event Store с Stream Projection

Реализовать:

  • Append-only event store (SQL или in-memory)
  • Optimistic concurrency control
  • Projection handler для build read model
  • Event processor для dispatch events к projections

Задание 2: CQRS с Separate Read/Write Models

Реализовать:

  • Write model с Event Sourcing
  • Read model с материализованными views
  • MediatR для command/query dispatch
  • Projection для синхронизации read/write

Задание 3: Event Versioning Handler

Реализовать:

  • Event upcaster pipeline
  • Поддержка нескольких версий событий
  • Backward compatibility
  • Migration strategy для existing events

System Design для .NET

Capacity Planning

RPS, Latency Budgets, Resource Estimation

Формулы:

RPS (Requests Per Second) = Total Requests / Time Window
        P99 Latency = 99th percentile response time
        Throughput = RPS * Average Response Size
        CPU Required = (RPS * Avg CPU Time per Request) / CPU Cores
        Memory Required = (Concurrent Requests * Memory per Request) + Base Memory

Пример: Capacity Plan для 1M DAU

Assumptions:
        - 1M Daily Active Users
        - Average 20 requests per user per day
        - Peak hour = 10% of daily traffic
        - P99 latency target = 200ms
        - Average response size = 50KB

        Calculations:
        - Daily requests = 1M * 20 = 20M
        - Peak hour requests = 20M * 0.1 = 2M
        - Peak RPS = 2M / 3600 = ~555 RPS
        - With 3x headroom = ~1,700 RPS

        Per-instance capacity (4 CPU, 8GB):
        - Each instance handles ~500 RPS (P99 < 200ms)
        - Instances needed = 1,700 / 500 = 4 (round up)
        - With redundancy = 6 instances

        Memory:
        - Per request: 50KB response + 100KB overhead = 150KB
        - Concurrent requests: 500 RPS * 0.2s avg = 100 concurrent
        - Memory per instance: 100 * 150KB + 500MB base = ~515MB
        - 8GB is more than enough

        Network:
        - Bandwidth: 1,700 RPS * 50KB = 85 MB/s = 680 Mbps
        - Need 1Gbps network interface

Little's Law

Concurrent Requests = RPS * Average Response Time

        Пример:
        - 1000 RPS
        - Avg response time = 50ms = 0.05s
        - Concurrent = 1000 * 0.05 = 50 concurrent requests

Horizontal vs Vertical Scaling

Vertical Scaling (Scale Up)

┌──────────────────────────────────────┐
        │           Single Server               │
        │  ┌────────────────────────────────┐  │
        │  │  More CPU, More RAM, More SSD  │  │
        │  └────────────────────────────────┘  │
        └──────────────────────────────────────┘

        Pros: Simple, no distributed system complexity
        Cons: Hard limit, single point of failure, expensive

Horizontal Scaling (Scale Out)

┌──────────┐  ┌──────────┐  ┌──────────┐
        │ Server 1 │  │ Server 2 │  │ Server 3 │
        │  (app)   │  │  (app)   │  │  (app)   │
        └────┬─────┘  └────┬─────┘  └────┬─────┘
             │             │             │
             └─────────────┼─────────────┘
                           │
                    ┌──────▼──────┐
                    │ Load Balancer│
                    └─────────────┘

        Pros: No hard limit, fault tolerance, cost-effective
        Cons: Distributed complexity, stateless required

Stateless Design для Horizontal Scaling

// Stateful — НЕ масштабируется горизонтально
        public class ShoppingCartService
        {
            private static readonly ConcurrentDictionary<string, Cart> _carts = new();
            // Data lost on restart, not shared between instances
        }

        // Stateless — масштабируется
        public class ShoppingCartService
        {
            private readonly ICartRepository _repository;
            private readonly IDistributedCache _cache;

            public async Task<Cart> GetCartAsync(string userId, CancellationToken ct = default)
            {
                var cacheKey = $"cart:{userId}";
                var cached = await _cache.GetStringAsync(cacheKey, ct);
                if (cached != null) return JsonSerializer.Deserialize<Cart>(cached);

                var cart = await _repository.GetByUserIdAsync(userId, ct);
                await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(cart), 
                    new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }, ct);
        
                return cart;
            }
        }

Caching Strategies

Multi-Level Caching

┌─────────────────────────────────────────────────────────┐
        │                    Client Browser                        │
        │              (Browser Cache, Service Worker)              │
        ├─────────────────────────────────────────────────────────┤
        │                       CDN                                │
        │            (CloudFlare, CloudFront, Akamai)              │
        ├─────────────────────────────────────────────────────────┤
        │                  Reverse Proxy (YARP)                    │
        │              (Response Caching, Output Cache)            │
        ├─────────────────────────────────────────────────────────┤
        │                  Application (IMemoryCache)              │
        │              (In-process, per-instance)                  │
        ├─────────────────────────────────────────────────────────┤
        │               Distributed Cache (Redis)                  │
        │              (Shared across instances)                   │
        ├─────────────────────────────────────────────────────────┤
        │                    Database                              │
        └─────────────────────────────────────────────────────────┘

Client-Side Caching

// HTTP Cache Headers
        app.Use(async (context, next) =>
        {
            if (context.Request.Path.StartsWithSegments("/api/public"))
            {
                context.Response.Headers["Cache-Control"] = "public, max-age=300"; // 5 min
                context.Response.Headers["ETag"] = GenerateETag(context.Request.Path);
            }
            await next();
        });

        // Conditional GET — If-None-Match
        public class ETagMiddleware
        {
            private readonly RequestDelegate _next;

            public async Task InvokeAsync(HttpContext context)
            {
                var ifNoneMatch = context.Request.Headers["If-None-Match"].ToString();
                var etag = GenerateETag(context.Request.Path);

                if (ifNoneMatch == etag)
                {
                    context.Response.StatusCode = 304; // Not Modified
                    return;
                }

                context.Response.Headers["ETag"] = etag;
                await _next(context);
            }
        }

CDN Caching

// Cache-Control для CDN
        [ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any)]
        public IActionResult GetProduct(string id)
        {
            return Ok(_products.GetById(id));
        }

        // Cache invalidation через CDN API
        public class CdnCacheInvalidator
        {
            private readonly HttpClient _cdnClient;

            public async Task InvalidateAsync(string path)
            {
                await _cdnClient.PostAsync("/purge", new StringContent(
                    JsonSerializer.Serialize(new { paths = new[] { path } })
                ));
            }
        }

Reverse Proxy Caching (YARP)

builder.Services.AddReverseProxy()
            .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
            .AddOutputCache(); // Response caching

        // Config
        "ReverseProxy": {
          "Routes": {
            "cached-route": {
              "ClusterId": "api-cluster",
              "Match": { "Path": "/api/products/{**catch-all}" },
              "OutputCache": { "PolicyName": "products" }
            }
          },
          "OutputCacheProfiles": {
            "products": {
              "Expire": "00:05:00",
              "VaryByHeader": "Accept-Encoding"
            }
          }
        }

Distributed Cache (Redis)

// NuGet: Microsoft.Extensions.Caching.StackExchangeRedis
        builder.Services.AddStackExchangeRedisCache(options =>
        {
            options.Configuration = "redis:6379";
            options.InstanceName = "MyApp:";
        });

        // Использование
        public class ProductService
        {
            private readonly IDistributedCache _cache;
            private readonly IProductRepository _repository;

            public async Task<Product> GetProductAsync(string id, CancellationToken ct = default)
            {
                var cacheKey = $"product:{id}";
                var cached = await _cache.GetStringAsync(cacheKey, ct);
        
                if (cached != null)
                    return JsonSerializer.Deserialize<Product>(cached);

                var product = await _repository.GetByIdAsync(id, ct);
        
                if (product != null)
                {
                    await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(product),
                        new DistributedCacheEntryOptions
                        {
                            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
                            SlidingExpiration = TimeSpan.FromMinutes(5)
                        }, ct);
                }

                return product;
            }

            public async Task UpdateProductAsync(Product product, CancellationToken ct = default)
            {
                await _repository.UpdateAsync(product, ct);
        
                // Invalidate cache
                await _cache.RemoveAsync($"product:{product.Id}", ct);
            }
        }

Cache Invalidation Patterns

Cache-Aside (Lazy Loading)
// Самый распространённый паттерн
        public async Task<T> GetAsync(string key)
        {
            // 1. Проверяем кэш
            var cached = await _cache.GetAsync<T>(key);
            if (cached != null) return cached;

            // 2. Cache miss — загружаем из источника
            var data = await _source.GetAsync(key);
    
            // 3. Сохраняем в кэш
            await _cache.SetAsync(key, data, expiration);
    
            return data;
        }
Write-Through
// Запись одновременно в кэш и источник
        public async Task SetAsync(string key, T value)
        {
            // 1. Записываем в источник
            await _source.SetAsync(key, value);
    
            // 2. Обновляем кэш
            await _cache.SetAsync(key, value, expiration);
        }

        // Pros: кэш всегда консистентен
        // Cons: медленнее запись
Write-Behind (Write-Back)
// Запись сначала в кэш, потом асинхронно в источник
        public class WriteBehindCache<T>
        {
            private readonly IDistributedCache _cache;
            private readonly Channel<CacheOperation> _writeChannel;

            public WriteBehindCache(IDistributedCache cache)
            {
                _cache = cache;
                _writeChannel = Channel.CreateBounded<CacheOperation>(1000);
                _ = ProcessWritesAsync();
            }

            public async Task SetAsync(string key, T value)
            {
                // Сразу обновляем кэш
                await _cache.SetAsync(key, value);
        
                // Асинхронно записываем в источник
                await _writeChannel.Writer.WriteAsync(new CacheOperation(key, value));
            }

            private async Task ProcessWritesAsync()
            {
                await foreach (var op in _writeChannel.Reader.ReadAllAsync())
                {
                    await _source.SetAsync(op.Key, op.Value);
                }
            }
        }

        // Pros: быстрая запись
        // Cons: возможна потеря данных при crash

Message Queue Patterns

Pub/Sub

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
        │  Publisher  │────▶│   Exchange   │────▶│  Consumer 1 │
        └─────────────┘     │   (Topic)    │     └─────────────┘
                            │              │     ┌─────────────┐
                            │              │────▶│  Consumer 2 │
                            └──────────────┘     └─────────────┘
// Pub/Sub с MassTransit
        builder.Services.AddMassTransit(x =>
        {
            x.UsingRabbitMq((context, cfg) =>
            {
                cfg.Host("rabbitmq", "/", h => { });
        
                cfg.ReceiveEndpoint("order-created-queue", e =>
                {
                    e.Subscribe<OrderCreatedEvent>();
                    e.Handler<OrderCreatedEvent>(context =>
                        context.Consumer<IOrderCreatedConsumer>());
                });
            });
        });

        // Publisher
        public class OrderService
        {
            private readonly IPublishEndpoint _publishEndpoint;

            public async Task CreateOrderAsync(CreateOrderCommand command)
            {
                var order = new Order(command.CustomerId);
                await _repository.SaveAsync(order);
        
                await _publishEndpoint.Publish(new OrderCreatedEvent(order.Id, order.CustomerId));
            }
        }

        // Consumer
        public class OrderCreatedConsumer : IConsumer<OrderCreatedEvent>
        {
            public async Task Consume(ConsumeContext<OrderCreatedEvent> context)
            {
                var @event = context.Message;
                // Handle event
            }
        }

Work Queues

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
        │  Producer   │────▶│    Queue     │────▶│  Worker 1   │
        └─────────────┘     │              │     └─────────────┘
                            │              │     ┌─────────────┐
                            │              │────▶│  Worker 2   │
                            └──────────────┘     └─────────────┘
// Work queue — один consumer получает каждое сообщение
        public class EmailWorker : BackgroundService
        {
            private readonly IModel _channel;
            private readonly IServiceProvider _serviceProvider;

            protected override Task ExecuteAsync(CancellationToken stoppingToken)
            {
                _channel.BasicQos(prefetchSize: 0, prefetchCount: 10, global: false);
        
                var consumer = new EventingBasicConsumer(_channel);
                consumer.Received += async (model, ea) =>
                {
                    var body = ea.Body.ToArray();
                    var message = JsonSerializer.Deserialize<EmailMessage>(body);
            
                    try
                    {
                        using var scope = _serviceProvider.CreateScope();
                        var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();
                        await emailService.SendAsync(message.To, message.Subject, message.Body);
                
                        _channel.BasicAck(ea.DeliveryTag, multiple: false);
                    }
                    catch
                    {
                        _channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: true);
                    }
                };

                _channel.BasicConsume("email-queue", autoAck: false, consumer: consumer);
                return Task.CompletedTask;
            }
        }

Dead Letter Queue

// DLQ — сообщения, которые не удалось обработать
        public class DlqHandler
        {
            private readonly IModel _channel;

            public void SetupDlq()
            {
                // Основная очередь с DLX
                var args = new Dictionary<string, object>
                {
                    { "x-dead-letter-exchange", "dlx" },
                    { "x-dead-letter-routing-key", "dlq" },
                    { "x-max-length", 10000 },
                    { "x-message-ttl", 60000 } // 60 seconds
                };

                _channel.QueueDeclare("main-queue", durable: true, exclusive: false, autoDelete: false, arguments: args);
        
                // DLQ
                _channel.ExchangeDeclare("dlx", ExchangeType.Direct);
                _channel.QueueDeclare("dlq", durable: true, exclusive: false, autoDelete: false);
                _channel.QueueBind("dlq", "dlx", "dlq");
            }

            // Retry из DLQ
            public async Task ProcessDlqAsync()
            {
                var result = _channel.BasicGet("dlq", autoAck: false);
                if (result == null) return;

                var message = JsonSerializer.Deserialize<FailedMessage>(result.Body.ToArray());
        
                if (message.RetryCount < 3)
                {
                    message.RetryCount++;
                    message.NextRetryAt = DateTime.UtcNow.AddMinutes(Math.Pow(2, message.RetryCount));
            
                    // Re-publish с delay
                    var properties = _channel.CreateBasicProperties();
                    properties.Headers = new Dictionary<string, object>
                    {
                        { "x-delay", (int)(message.NextRetryAt - DateTime.UtcNow).TotalMilliseconds }
                    };

                    _channel.BasicPublish("", "main-queue", properties, 
                        JsonSerializer.SerializeToUtf8Bytes(message));
                }
                else
                {
                    // Max retries reached — log and discard
                    _channel.BasicAck(result.DeliveryTag, multiple: false);
                }
            }
        }

Database Scaling

Read Replicas

┌─────────────┐     ┌──────────────┐
        │   Writes    │────▶│   Primary    │
        │             │     │   (Master)   │
        └─────────────┘     └──────┬───────┘
                                   │ Replication
                            ┌──────┴───────┐
                            │              │
                      ┌─────▼─────┐  ┌────▼─────┐
                      │  Replica 1 │  │ Replica 2│
                      │  (Reads)   │  │ (Reads)  │
                      └────────────┘  └──────────┘
// Read/Write splitting в EF Core
        public class ReadWriteDbContext : DbContext
        {
            private readonly IHttpContextAccessor _httpContext;

            public ReadWriteDbContext(DbContextOptions<ReadWriteDbContext> options, IHttpContextAccessor httpContext)
                : base(options)
            {
                _httpContext = httpContext;
            }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                // GET запросы → replica, POST/PUT/DELETE → primary
                var method = _httpContext.HttpContext?.Request.Method;
                var connectionString = IsReadOperation(method) 
                    ? GetReadReplicaConnectionString() 
                    : GetPrimaryConnectionString();
        
                optionsBuilder.UseSqlServer(connectionString);
            }

            private bool IsReadOperation(string method) => 
                method == HttpMethods.Get || method == HttpMethods.Head;
        }

Sharding

┌─────────────────────────────────────────────────────┐
        │                  Shard Router                        │
        │                                                       │
        │  Tenant A ──▶ Shard 1 (DB-A)                         │
        │  Tenant B ──▶ Shard 1 (DB-A)                         │
        │  Tenant C ──▶ Shard 2 (DB-B)                         │
        │  Tenant D ──▶ Shard 2 (DB-B)                         │
        └─────────────────────────────────────────────────────┘
public class ShardingDbContext : DbContext
        {
            private readonly ITenantContext _tenantContext;
            private readonly IShardResolver _shardResolver;

            public ShardingDbContext(
                DbContextOptions options,
                ITenantContext tenantContext,
                IShardResolver shardResolver)
                : base(options)
            {
                _tenantContext = tenantContext;
                _shardResolver = shardResolver;
            }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                var shard = _shardResolver.ResolveShard(_tenantContext.Tenant.Id);
                optionsBuilder.UseSqlServer(shard.ConnectionString);
            }
        }

        public interface IShardResolver
        {
            ShardInfo ResolveShard(Guid tenantId);
        }

        public class ConsistentHashShardResolver : IShardResolver
        {
            private readonly List<ShardInfo> _shards;

            public ShardInfo ResolveShard(Guid tenantId)
            {
                var hash = GetHash(tenantId);
                var index = hash % _shards.Count;
                return _shards[index];
            }

            private int GetHash(Guid tenantId)
            {
                var bytes = tenantId.ToByteArray();
                return Math.Abs(BitConverter.ToInt32(bytes, 0));
            }
        }

Partitioning

-- Range Partitioning (по дате)
        CREATE TABLE Orders (
            Id INT,
            OrderDate DATE,
            CustomerId INT,
            Total DECIMAL
        ) PARTITION BY RANGE (YEAR(OrderDate)) (
            PARTITION p2023 VALUES LESS THAN (2024),
            PARTITION p2024 VALUES LESS THAN (2025),
            PARTITION p2025 VALUES LESS THAN (2026)
        );

        -- Hash Partitioning (для равномерного распределения)
        CREATE TABLE Users (
            Id INT,
            Name VARCHAR(100),
            Email VARCHAR(100)
        ) PARTITION BY HASH (Id) PARTITIONS 4;

        -- List Partitioning (по региону)
        CREATE TABLE Stores (
            Id INT,
            Name VARCHAR(100),
            Region VARCHAR(50)
        ) PARTITION BY LIST (Region) (
            PARTITION p_east VALUES IN ('NY', 'MA', 'CT'),
            PARTITION p_west VALUES IN ('CA', 'WA', 'OR'),
            PARTITION p_central VALUES IN ('TX', 'IL', 'OH')
        );

Практика

Задание 1: System Design для 1M DAU

Спроектировать систему с capacity plan:

  • RPS calculation с peak factor
  • Instance sizing и count
  • Database sizing (connections, storage, IOPS)
  • Network bandwidth requirements
  • Cost estimation

Задание 2: Multi-Level Caching Strategy

Реализовать:

  • Browser cache с ETag/Last-Modified
  • CDN cache с invalidation
  • Application IMemoryCache
  • Distributed Redis cache
  • Cache-aside pattern
  • Write-through для critical data

Задание 3: Load Balancer Simulation

Создать load balancer simulation с:

  • Health checks (liveness + readiness)
  • Circuit breaking для unhealthy instances
  • Load balancing algorithms (round-robin, least-connections, weighted)
  • Instance registration/deregistration

Observability Architecture

Three Pillars: Logs, Metrics, Traces

┌─────────────────────────────────────────────────────┐
        │                  Observability                        │
        │                                                       │
        │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │
        │  │    Logs     │  │  Metrics    │  │   Traces    │  │
        │  │             │  │             │  │             │  │
        │  │ "What       │  │ "How        │  │ "Why"       │  │
        │  │  happened?" │  │  much?"     │  │             │  │
        │  │             │  │             │  │             │  │
        │  │ Structured  │  │ Numeric,    │  │ Request     │  │
        │  │ text with   │  │ aggregated, │  │ flow across │  │
        │  │ context     │  │ time-series │  │ services    │  │
        │  └─────────────┘  └─────────────┘  └─────────────┘  │
        └─────────────────────────────────────────────────────┘
PillarЧто отвечаетПримерИнструменты
LogsЧто произошло?"Order 123 failed: timeout"Serilog, Seq, ELK
MetricsСколько/как часто?"P99 latency = 250ms"Prometheus, Grafana
TracesПочему/где?"Request flow: API → Order → Payment"Jaeger, Zipkin

OpenTelemetry

Instrumentation

// NuGet: OpenTelemetry.Extensions.Hosting
        // NuGet: OpenTelemetry.Instrumentation.AspNetCore
        // NuGet: OpenTelemetry.Instrumentation.Http
        // NuGet: OpenTelemetry.Instrumentation.EntityFrameworkCore

        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddOpenTelemetry()
            .WithTracing(tracing => tracing
                .AddSource("MyApp")
                .AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddEntityFrameworkCoreInstrumentation()
                .AddJaegerExporter()) // или OTLP
            .WithMetrics(metrics => metrics
                .AddMeter("MyApp")
                .AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddRuntimeInstrumentation()
                .AddProcessInstrumentation()
                .AddPrometheusExporter());

Custom Instrumentation

public class OrderService
        {
            private static readonly ActivitySource ActivitySource = new("MyApp", "1.0.0");
            private static readonly Meter Meter = new("MyApp", "1.0.0");
    
            private static readonly Counter<long> OrdersCreatedCounter = 
                Meter.CreateCounter<long>("orders.created", description: "Number of orders created");
    
            private static readonly Histogram<double> OrderProcessingDuration = 
                Meter.CreateHistogram<double>("orders.processing.duration.ms", 
                    description: "Order processing duration");

            public async Task<Order> CreateOrderAsync(CreateOrderCommand command, CancellationToken ct = default)
            {
                using var activity = ActivitySource.StartActivity("CreateOrder");
                activity?.SetTag("customer.id", command.CustomerId);
                activity?.SetTag("order.items.count", command.Items.Count);
        
                var stopwatch = Stopwatch.StartNew();
        
                try
                {
                    var order = new Order(command.CustomerId);
            
                    foreach (var item in command.Items)
                    {
                        order.AddItem(item.ProductId, item.Quantity, item.Price);
                        activity?.AddEvent(new ActivityEvent("ItemAdded", tags: new ActivityTagsCollection
                        {
                            { "product.id", item.ProductId },
                            { "quantity", item.Quantity }
                        }));
                    }

                    await _repository.SaveAsync(order, ct);
            
                    OrdersCreatedCounter.Add(1);
                    activity?.SetStatus(ActivityStatusCode.Ok);
            
                    return order;
                }
                catch (Exception ex)
                {
                    activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
                    activity?.RecordException(ex);
                    throw;
                }
                finally
                {
                    stopwatch.Stop();
                    OrderProcessingDuration.Record(stopwatch.ElapsedMilliseconds);
                }
            }
        }

Baggage — передача контекста

// Baggage — метаданные, которые propagate через все сервисы
        public class OrderController : ControllerBase
        {
            [HttpPost("orders")]
            public async Task<ActionResult> CreateOrder(CreateOrderCommand command)
            {
                // Устанавливаем baggage
                Baggage.SetItem("user.id", User.GetUserId());
                Baggage.SetItem("user.tier", User.GetTier());
                Baggage.SetItem("request.source", "web");

                var order = await _orderService.CreateOrderAsync(command);
                return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
            }
        }

        // В другом сервисе — читаем baggage
        public class PaymentService
        {
            public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
            {
                var userId = Baggage.GetItem("user.id");
                var userTier = Baggage.GetItem("user.tier");
        
                // Premium users get priority processing
                if (userTier == "premium")
                {
                    return await ProcessPriorityPaymentAsync(request);
                }
        
                return await ProcessStandardPaymentAsync(request);
            }
        }

Distributed Tracing

W3C Trace Context

Trace ID: 4bf92f3577b34da6a3ce929d0e0e4736
        Span ID:  00f067aa0ba902b7
        Parent:   0000000000000000

        HTTP Headers:
        traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
        tracestate:  vendor1=value1,vendor2=value2
Request → API Gateway → Order Service → Payment Service → Email Service
           │           │              │              │               │
           │───traceparent header propagates automatically ──────────│
           │                                                          │
           └─── Full Trace:                                           │
               TraceId: abc123...                                     │
               ├── Span 1: API Gateway (0-50ms)                       │
               │   └── Span 2: Order Service (10-200ms)               │
               │       ├── Span 3: DB Query (20-45ms)                 │
               │       └── Span 4: Payment Service (50-180ms)         │
               │           ├── Span 5: Stripe API (60-150ms)          │
               │           └── Span 6: Email Service (160-175ms)      │
               └─── Total: 200ms                                      │

Propagation в .NET

// .NET автоматически propagates trace context через HttpClient
        // Но нужно настроить для gRPC и message brokers

        // gRPC propagation
        builder.Services.AddGrpcClient<OrderService.OrderServiceClient>(options =>
        {
            options.Address = new Uri("https://order-service:5001");
        })
        .AddInterceptor<GrpcTraceInterceptor>();

        public class GrpcTraceInterceptor : Interceptor
        {
            public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
                TRequest request,
                ClientInterceptorContext<TRequest, TResponse> context,
                AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
            {
                var metadata = context.Options.Headers ?? new Metadata();
        
                // Inject current trace context
                if (Activity.Current != null)
                {
                    metadata.Add("traceparent", 
                        $"00-{Activity.Current.TraceId}-{Activity.Current.SpanId}-01");
                }

                var newOptions = context.Options.WithHeaders(metadata);
                var newContext = new ClientInterceptorContext<TRequest, TResponse>(
                    context.Method, context.Host, newOptions);

                return continuation(request, newContext);
            }
        }

        // RabbitMQ propagation
        public class RabbitMqTracePublisher : IEventPublisher
        {
            private readonly IModel _channel;

            public async Task PublishAsync<T>(T @event, string routingKey)
            {
                var properties = _channel.CreateBasicProperties();
        
                if (Activity.Current != null)
                {
                    properties.Headers = new Dictionary<string, object>
                    {
                        { "traceparent", $"00-{Activity.Current.TraceId}-{Activity.Current.SpanId}-01" }
                    };
                }

                var body = JsonSerializer.SerializeToUtf8Bytes(@event);
                _channel.BasicPublish("order-events", routingKey, properties, body);
            }
        }

        // RabbitMQ consumer — извлекает trace context
        public class TraceAwareConsumer : BackgroundService
        {
            protected override Task ExecuteAsync(CancellationToken stoppingToken)
            {
                var consumer = new AsyncEventingBasicConsumer(_channel);
                consumer.Received += async (model, ea) =>
                {
                    // Extract trace context
                    if (ea.BasicProperties.Headers?.TryGetValue("traceparent", out var traceparent) == true)
                    {
                        var traceContext = traceparent.ToString();
                        // Parse and create Activity from traceparent
                        var activity = new Activity("ProcessMessage");
                        activity.SetParentId(traceContext);
                        activity.Start();
                    }

                    try
                    {
                        await ProcessMessageAsync(ea);
                        _channel.BasicAck(ea.DeliveryTag, false);
                    }
                    catch
                    {
                        _channel.BasicNack(ea.DeliveryTag, false, true);
                    }
                    finally
                    {
                        Activity.Current?.Stop();
                    }
                };

                _channel.BasicConsume("queue", false, consumer);
                return Task.CompletedTask;
            }
        }

Structured Logging — Serilog

Настройка

// NuGet: Serilog.AspNetCore
        // NuGet: Serilog.Sinks.Console
        // NuGet: Serilog.Sinks.File
        // NuGet: Serilog.Sinks.Seq
        // NuGet: Serilog.Enrichers.Environment
        // NuGet: Serilog.Enrichers.Thread
        // NuGet: Serilog.Enrichers.Span

        builder.Host.UseSerilog((context, services, configuration) => configuration
            .ReadFrom.Configuration(context.Configuration)
            .ReadFrom.Services(services)
            .Enrich.FromLogContext()
            .Enrich.WithEnvironmentName()
            .Enrich.WithMachineName()
            .Enrich.WithThreadId()
            .Enrich.WithSpan()  // OpenTelemetry correlation
            .WriteTo.Console(
                outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
            .WriteTo.File(
                path: "logs/app-.log",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 30)
            .WriteTo.Seq("http://seq:5341"));

Structured Logging

public class OrderService
        {
            private readonly ILogger<OrderService> _logger;

            public async Task<Order> CreateOrderAsync(CreateOrderCommand command)
            {
                // Structured logging — свойства, не строки
                _logger.LogInformation(
                    "Creating order for customer {CustomerId} with {ItemCount} items",
                    command.CustomerId, command.Items.Count);

                try
                {
                    var order = new Order(command.CustomerId);
            
                    foreach (var item in command.Items)
                    {
                        order.AddItem(item.ProductId, item.Quantity, item.Price);
                
                        _logger.LogDebug(
                            "Added item {ProductId} x{Quantity} @ {Price} to order {OrderId}",
                            item.ProductId, item.Quantity, item.Price, order.Id);
                    }

                    await _repository.SaveAsync(order);
            
                    _logger.LogInformation(
                        "Order {OrderId} created successfully with total {Total}",
                        order.Id, order.Total);
            
                    return order;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex,
                        "Failed to create order for customer {CustomerId}",
                        command.CustomerId);
                    throw;
                }
            }
        }

Log Enrichers

// Кастомный enricher
        public class TenantEnricher : ILogEventEnricher
        {
            private readonly ITenantContext _tenantContext;

            public TenantEnricher(ITenantContext tenantContext) => _tenantContext = tenantContext;

            public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
            {
                if (_tenantContext.Tenant != null)
                {
                    logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
                        "TenantId", _tenantContext.Tenant.Id));
                    logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
                        "TenantName", _tenantContext.Tenant.Name));
                }
            }
        }

        // Request enricher
        public class RequestEnricher : ILogEventEnricher
        {
            private readonly IHttpContextAccessor _httpContext;

            public RequestEnricher(IHttpContextAccessor httpContext) => _httpContext = httpContext;

            public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
            {
                var context = _httpContext.HttpContext;
                if (context == null) return;

                logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
                    "RequestId", context.TraceIdentifier));
                logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
                    "RequestMethod", context.Request.Method));
                logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
                    "RequestPath", context.Request.Path));
                logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
                    "UserAgent", context.Request.Headers["User-Agent"].ToString()));
            }
        }

        // Регистрация
        configuration.Enrich.With(new TenantEnricher(tenantContext));
        configuration.Enrich.With(new RequestEnricher(httpContext));

Health Checks

Liveness vs Readiness

Liveness Probe: "Is the process alive?"
        - Если NO → restart container
        - Проверяет: процесс не crashed, нет deadlock

        Readiness Probe: "Is the service ready to accept traffic?"
        - Если NO → remove from load balancer
        - Проверяет: БД доступна, кэш подключён, зависимости healthy

Implementation

builder.Services.AddHealthChecks()
            // Liveness — только базовая проверка
            .AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "liveness" })
    
            // Readiness — все зависимости
            .AddSqlServer(builder.Configuration.GetConnectionString("Default"), 
                name: "database", tags: new[] { "readiness" })
            .AddRedis(builder.Configuration.GetConnectionString("Redis"),
                name: "redis", tags: new[] { "readiness" })
            .AddUrlGroup(new Uri("https://payment-service/health"),
                name: "payment-service", tags: new[] { "readiness" })
            .AddDbContextCheck<AppDbContext>(
                name: "dbcontext", tags: new[] { "readiness" });

        var app = builder.Build();

        // Liveness endpoint — lightweight
        app.MapHealthChecks("/health/live", new HealthCheckOptions
        {
            Predicate = check => check.Tags.Contains("liveness"),
            ResponseWriter = WriteHealthResponse
        });

        // Readiness endpoint — full check
        app.MapHealthChecks("/health/ready", new HealthCheckOptions
        {
            Predicate = check => check.Tags.Contains("readiness"),
            ResponseWriter = WriteHealthResponse
        });

        // Full health — для admin dashboard
        app.MapHealthChecks("/health", new HealthCheckOptions
        {
            ResponseWriter = WriteHealthResponse
        });

        static Task WriteHealthResponse(HttpContext context, HealthReport report)
        {
            context.Response.ContentType = "application/json";
            var json = JsonSerializer.Serialize(new
            {
                status = report.Status.ToString(),
                checks = report.Entries.Select(e => new
                {
                    name = e.Key,
                    status = e.Value.Status.ToString(),
                    duration = e.Value.Duration.TotalMilliseconds,
                    description = e.Value.Description,
                    data = e.Value.Data
                }),
                totalDuration = report.TotalDuration.TotalMilliseconds
            });
            return context.Response.WriteAsync(json);
        }

Custom Health Check

public class OrderQueueHealthCheck : IHealthCheck
        {
            private readonly IOrderQueue _queue;
            private readonly ILogger<OrderQueueHealthCheck> _logger;

            public async Task<HealthCheckResult> CheckHealthAsync(
                HealthCheckContext context, CancellationToken ct = default)
            {
                try
                {
                    var queueDepth = await _queue.GetDepthAsync(ct);
            
                    if (queueDepth > 10000)
                    {
                        return HealthCheckResult.Degraded(
                            $"Order queue depth is {queueDepth} (threshold: 10000)");
                    }

                    if (queueDepth > 50000)
                    {
                        return HealthCheckResult.Unhealthy(
                            $"Order queue depth is {queueDepth} (critical: 50000)");
                    }

                    return HealthCheckResult.Healthy($"Queue depth: {queueDepth}");
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Failed to check order queue health");
                    return HealthCheckResult.Unhealthy("Cannot connect to order queue", ex);
                }
            }
        }

Metrics — Prometheus + Grafana

Custom Metrics

public class OrderMetrics
        {
            private static readonly Meter Meter = new("MyApp.Orders", "1.0.0");

            private static readonly Counter<long> _ordersCreated = 
                Meter.CreateCounter<long>("orders.created.total", "orders", "Total orders created");
    
            private static readonly Counter<long> _ordersConfirmed = 
                Meter.CreateCounter<long>("orders.confirmed.total", "orders", "Total orders confirmed");
    
            private static readonly Counter<long> _ordersCancelled = 
                Meter.CreateCounter<long>("orders.cancelled.total", "orders", "Total orders cancelled");
    
            private static readonly Histogram<double> _orderValue = 
                Meter.CreateHistogram<double>("orders.value", "USD", "Order total value");
    
            private static readonly Histogram<double> _orderProcessingTime = 
                Meter.CreateHistogram<double>("orders.processing.time.ms", "ms", "Order processing time");

            public static void RecordOrderCreated(decimal total)
            {
                _ordersCreated.Add(1);
                _orderValue.Record((double)total);
            }

            public static void RecordOrderConfirmed() => _ordersConfirmed.Add(1);
            public static void RecordOrderCancelled() => _ordersCancelled.Add(1);

            public static void RecordProcessingTime(double milliseconds) => 
                _orderProcessingTime.Record(milliseconds);
        }

Prometheus Exporter

// NuGet: OpenTelemetry.Exporter.Prometheus.AspNetCore

        builder.Services.AddOpenTelemetry()
            .WithMetrics(metrics => metrics
                .AddMeter("MyApp.Orders")
                .AddAspNetCoreInstrumentation()
                .AddRuntimeInstrumentation()
                .AddPrometheusExporter());

        var app = builder.Build();

        // Prometheus scraping endpoint
        app.MapPrometheusScrapingEndpoint();

        // Prometheus config
        /*
        scrape_configs:
          - job_name: 'myapp'
            scrape_interval: 15s
            static_configs:
              - targets: ['myapp:5000']
        */

Grafana Dashboard

{
          "dashboard": {
            "title": "Order Service Dashboard",
            "panels": [
              {
                "title": "Orders per Second",
                "type": "graph",
                "targets": [{
                  "expr": "rate(orders_created_total[1m])"
                }]
              },
              {
                "title": "P99 Processing Time",
                "type": "graph",
                "targets": [{
                  "expr": "histogram_quantile(0.99, rate(orders_processing_time_ms_bucket[5m]))"
                }]
              },
              {
                "title": "Order Value Distribution",
                "type": "heatmap",
                "targets": [{
                  "expr": "rate(orders_value_bucket[5m])"
                }]
              },
              {
                "title": "Error Rate",
                "type": "singlestat",
                "targets": [{
                  "expr": "rate(http_server_requests_seconds_count{status=~\"5..\"}[5m]) / rate(http_server_requests_seconds_count[5m]) * 100"
                }]
              }
            ]
          }
        }

Практика

Задание 1: End-to-End Distributed Tracing

Настроить distributed tracing для microservice system:

  • OpenTelemetry instrumentation для всех сервисов
  • Trace context propagation через HTTP, gRPC, message brokers
  • Jaeger/Zipkin UI для визуализации
  • Correlation ID для логов

Задание 2: Custom OpenTelemetry Instrumentation

Реализовать custom instrumentation:

  • ActivitySource для business operations
  • Meter с Counter, Histogram, Gauge
  • Baggage для user context propagation
  • Custom span events и attributes

Задание 3: Observability Dashboard

Создать observability dashboard с key business metrics:

  • Prometheus metrics export
  • Grafana dashboard panels
  • Alert rules (error rate > 1%, latency > 500ms)
  • Business metrics (orders/hour, revenue, conversion rate)

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

Проект: Ядро SaaS платформы
  • Clean Architecture + DDD для domain layer
  • Multi-tenancy с tenant isolation
  • CQRS + Event Sourcing для core aggregate
  • Resilient external integrations через Polly
  • Full observability (logs, metrics, traces)
  • API Gateway с authentication и rate limiting
Критерии прохождения:
  • Architecture decision records (ADR) для каждого major решения
  • Domain model проходит review на DDD correctness
  • System handles graceful degradation при failures
  • Distributed trace показывает full request flow
  • Load test подтверждает architecture scalability claims