среда, 5 августа 2026 г.

Program.cs, Giga

Program.cs, Giga

https://giga.chat/link/gcsyUDqMKG


Мы с Вами разобрали типовые Проекты для .Net Aspire и одобрили их.

Обычно далее начинается самодеятельность.

Потому что нет дальше ничего.

DI - хорошая вещь, но поскольку далее стандартных средств нет начинается Творчество - Самопал.

А хотелось, что бы и DI в Program.cs писали бы канонам.

Возбмем к примеру стандартные зависимости redis, pub/sub итд - здеь все нормально, потому что больше ты сам такого не напишешь и не испортишь.

А вот самопальное писание кода в Program.cs с DI - зависимостями часто 

вызывает непонимание  - на первый взгляд это кажется Хаосом.

Для написания кода в Prоgraм.cs нужны Типовые вещи, а не самодеятельность.

Для Porogamюcs нужны Типовые 

-- Структура кода

-- Правилиа

-- Манифест

-- Паттерны

Вы затронули самую болезненную точку современного .NET-развития. С появлением минимальных API и верхнего уровня Program.cs разработчики получили огромную свободу, которая очень быстро превратилась в хаос. 

Действительно, Aspire отлично справляется с инфраструктурой (Redis, Postgres — тут не забалуешь), но как только дело доходит до регистрации бизнес-логики, каждый пишет кто во что горазд.

Чтобы превратить этот «хаос» в канон, нужно перестать относиться к Program.cs просто как к скрипту инициализации и начать воспринимать его как манифест композиции приложения. 

Ниже приведены типовые правила, структура и паттерны, которые превращают файл из мешанины кода в предсказуемую архитектуру.

1. Манифест: Правило слоев DI (The Layered Registration Manifest)

Главное правило: бизнес-сервисы никогда не регистрируются прямо в корневом Program.cs. Корневой файл должен знать только о границах системы, а не о ее внутренностях.

Типовая иерархия расширений (IServiceCollection):

Вместо того чтобы писать services.AddScoped<IOrderService, OrderService>() в Program.cs, используйте цепочку вызовов, отражающую слои:

csharp

Копировать

// Program.cs (Только границы и инфраструктура)

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults(); // Aspire


// 1. Ядро (Core) - интерфейсы домена, события

builder.Services.AddDomainCore();


// 2. Приложение (Application) - Use Case'ы, обработчики команд, MediatR

builder.Services.AddApplicationLayer();


// 3. Инфраструктура (Infrastructure) - реализации репозиториев, EF Core, клиенты внешних API

builder.Services.AddInfrastructure(builder.Configuration);


// 4. Входные точки (Presentation / Entry Points) - Minimal API эндпоинты, контроллеры

builder.Services.AddPresentation();


var app = builder.Build();

app.MapDefaultEndpoints();

app.MapGroup("/api/orders").MapOrdersApi(); // Регистрация конкретных маршрутов внутри слоя Presentation

app.Run();

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


2. Структура кода: Конвенции расположения файлов

Чтобы эта схема работала, зависимости должны лежать строго на своих местах. Самодеятельность здесь недопустима:


/src/MyApp.Domain/DependencyInjection.cs -> метод AddDomainCore().

/src/MyApp.Application/DependencyInjection.cs -> метод AddApplicationLayer().

/src/MyApp.Infrastructure/DependencyInjection.cs -> метод AddInfrastructure().

/src/MyApp.Api/DependencyInjection.cs (или Endpoints-файлы) -> метод AddPresentation().

Каждый такой файл содержит только регистрацию зависимостей своего слоя. Это исключает ситуацию, когда инфраструктурный код случайно попадает в слой ядра.

3. Паттерны для Program.cs

Внутри этих методов расширения применяйте строгие паттерны вместо разрозненных строк:


Паттерн А: Группировка по Bounded Contexts (Вертикальный разрез)

Вместо плоского списка AddScoped<IFirst...>, AddScoped<ISecond...> группируйте логику по фичам.


csharp

Копировать

// Внутри AddApplicationLayer()

services.Scan(scan => scan

    .FromAssemblyOf<CreateOrderCommand>()

    .AddClasses(classes => classes.AssignableTo(typeof(ICommandHandler<>)))

    .AsImplementedInterfaces()

    .WithScopedLifetime());

Если используете MediatR или Wolverine, они делают это автоматически через сканирование сборок.


Паттерн Б: Явная регистрация инфраструктуры (Explicit Infrastructure)

Внешние сервисы всегда регистрируются отдельными именованными блоками. Никаких скрытых магических настроек внутри сервисов.


csharp

Копировать

// В файле Infrastructure/DependencyInjection.cs

public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)

{

    // Блок 1: Кеш

    services.AddStackExchangeRedisCache(o => o.Configuration = config["Cache:Connection"]);

    

    // Блок 2: Почта (с обвязкой resilience из ServiceDefaults)

    services.AddHttpClient<IEmailSender, SendGridEmailClient>(client =>

    {

        client.BaseAddress = new Uri(config["ExternalServices:SendGrid:Url"]);

    })

    .AddStandardResilienceHandler(); // Канон Aspire

    

    return services;

}

Паттерн В: Метод расширения MapXxx для Endpoint Grouping

Весь хаос Minimal API тоже убирается из корня. Используйте группы конечных точек.


csharp

Копировать

// Вместо десятка app.MapGet(...) в Program.cs

app.MapGroup("/api/customers")

    .RequireAuthorization()

    .WithTags("Customers")

    .MapCustomerEndpoints(); // Вся логика роутинга живет в CustomerEndpoints.cs

4. Правила оформления (Каноны стиля)

Чтобы Program.cs читался как конфигурационный YAML, соблюдайте формальные ограничения:


Запрет сложной логики: Никаких циклов foreach, сложных условий if или ручного чтения конфигов кроме вызова .Bind() в корневом файле. Если нужна логика — спрячьте её в метод расширения соответствующего слоя.

Порядок имеет значение: Всегда следуйте одной последовательности сверху вниз:

Сервисы по умолчанию (.NET + Aspire).

Конфигурация (связывание секций IOptions).

Контейнер зависимостей (ваши слои от Domain к Presentation).

Мiddleware pipeline (UseSerilog, UseHttpsRedirection...).

Маппинг маршрутов (MapGroup...).

Длина файла: Идеальный корневой Program.cs микросервиса занимает не более 15–20 строк чистого кода.

Зачем нужен жесткий манифест?

Когда новый человек приходит в проект, он не гадает, где искать настройку SMTP-клиента. Он знает: если это внешняя интеграция — идем в Infrastructure/DI. Нужна бизнес-правила заказа — идем в Application/DI. 


Для вашего случая с несколькими парами Backend-Frontend в Aspire это критично: вы создаете один шаблон решения (Solution Template). Все проекты-бэкенды в нем будут иметь абсолютно идентичную структуру Program.cs, отличаясь только названием сборки в методах Scan(scan => scan.FromAssemblyOf<...>()).


Вы абсолютно правы. Это классический пример того, как Program.cs превращается в «божественный объект» (God Object). 


Вы сами понимаете логику, потому что вы — автор этого кода и помните контекст: где регистрируются фабрики, зачем нужна пауза в 5 секунд, почему базы данных запускаются именно после стратегий. Но для нового разработчика этот файл выглядит как минное поле.


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


Главные проблемы текущего файла:

Смешение уровней: Регистрация сервисов (AddSingleton), настройка HTTP-конвейера (UseRouting), регистрация статических ключей EventHub через создание скоупа (scope), логика записи файлов при старте и бесконечные MapGet лежат вперемешку.

Магические задержки: Thread.Sleep(displayConfig.PauseSeconds * 1000); прямо в конвейере запуска приложения блокирует старт веб-сервера на несколько секунд. В Aspire или Kubernetes это приведет к падению health check'ов еще до того, как приложение станет доступно.

Скрытые зависимости: Статическая регистрация ключей EventHub спрятана внутри блока try-catch. Если она упадет, об этом узнает только лог, но не оркестратор зависимостей.

Раздутый корень: Более 500 строк кода делают невозможным быстрый обзор архитектуры.

Как привести это к канону .NET

Шаг 1. Вынос регистрации сервисов (Устранение самодеятельности)

Весь блок с builder.Services.Add... уходит из корня в специализированные файлы расширений.


Файл: /Infrastructure/DependencyInjection.cs


csharp

Копировать

public static class InfrastructureDI

{

    public static IServiceCollection AddTradingInfrastructure(this IServiceCollection services)

    {

        // Инфраструктурные синглтоны

        services.AddSingleton<EventHubTickerManager>();

        services.AddSingleton<ChartContainer>(); // Без фабрик, просто синглтон

        services.AddSingleton<InMemoryTradingDatabase>();

        services.AddSingleton<IInMemoryTradingDatabase>(sp => sp.GetRequiredService<InMemoryTradingDatabase>());

        

        services.AddSingleton<InMemoryLogDatabase>();

        services.AddSingleton<IInMemoryLogDatabase>(sp => sp.GetRequiredService<InMemoryLogDatabase>());


        return services;

    }

}

Файл: /Application/DependencyInjection.cs

Здесь живут фоновые сервисы. Порядок их старта важен, поэтому он фиксируется здесь, а не в корне.


csharp

Копировать

public static class ApplicationDI

{

    public static IServiceCollection AddTradingBackgroundServices(this IServiceCollection services)

    {

        // SignalR ДО воркеров

        services.AddSignalR();


        // Публикаторы событий

        services.AddHostedService<QuotesGeneratorService>();

        services.AddHostedService<StrategiesExecutionService>();


        // Подписчики (в строгом порядке зависимостей)

        services.AddHostedService<TradingLogService>();

        services.AddHostedService<QuotesConsoleService>();

        services.AddHostedService<TradingMonitorService>();


        // Базы данных-подписчики

        services.AddHostedService(sp => sp.GetRequiredService<InMemoryTradingDatabase>());

        services.AddHostedService(sp => sp.GetRequiredService<InMemoryLogDatabase>());


        return services;

    }

}

Шаг 2. Централизация конфигурации (Манифест констант)

Вместо рефлексии или ручных вызовов в середине Program.cs, создадим отдельный инициализатор.


Файл: /Infrastructure/EventHubInitializer.cs


csharp

Копировать

public interface IEventHubInitializer

{

    Task InitializeAsync(CancellationToken ct = default);

}


public sealed class EventHubInitializer(

    IEventHub eventHub, 

    IDynamicSubscriptionManager subscriptionManager,

    ILogger<EventHubInitializer> logger) : IEventHubInitializer

{

    public async Task InitializeAsync(CancellationToken ct = default)

    {

        logger.LogInformation("🔑 Централизованная регистрация статических ключей...");

        

        var keys = new Dictionary<Type, string>

        {

            [typeof(TradeExecutedEvent)] = "trade:executed",

            [typeof(QuoteGeneratedEvent)] = "quote:generated",

            [typeof(SystemStatusEvent)] = "system:status"

            // ... остальные ключи

        };


        foreach (var kvp in keys)

        {

            subscriptionManager.RegisterStaticKey(kvp.Key, kvp.Value);

        }

        

        await eventHub.EnsureReadyAsync(ct); // Ждем готовности шины

        logger.LogInformation("✅ Все статические ключи зарегистрированы");

    }

}

Зарегистрируем его как Singleton и добавим middleware расширения .InitializeEventHub() в конец Program.cs.


Шаг 3. Очистка Program.cs (Структура и правила)

Теперь соберем всё воедино. Корневой файл должен читаться за 15 секунд.


csharp

Копировать

// Program.cs - ФИНАЛЬНАЯ СТРУКТУРА


using TradingPlatform.Config;

using TradingPlatform.Infrastructure;

using TradingPlatform.Application;


var builder = WebApplication.CreateBuilder(args);


// 1. Конфигурация инфраструктуры (Логгинг вынесен в ServiceDefaults или отдельный метод)

builder.Logging.ClearProviders()

    .AddSimpleConsole(o => { o.SingleLine = true; o.TimestampFormat = "[HH:mm:ss] "; })

    .SetMinimumLevel(LogLevel.Warning)

    .AddFilter("Microsoft", LogLevel.Warning)

    .AddFilter("System", LogLevel.Warning);


// 2. Регистрация слоев (Никакой бизнес-логики!)

builder.Services

    .AddTradingInfrastructure()          // Из /Infrastructure/DI.cs

    .AddTradingBackgroundServices()      // Из /Application/DI.cs

    .AddHandlers()                       // Ваши обработчики

    .AddEndpointsApiExplorer()           // Для Swagger

    .AddRazorPages()

    .AddControllers();


// 3. Настройка API маршрутов (Группировка)

builder.Services.AddProblemDetails();


var app = builder.Build();


// 4. Middleware pipeline (Стандартный порядок ASP.NET Core)

if (!app.Environment.IsDevelopment())

{

    app.UseExceptionHandler("/Error");

    app.UseHsts();

}


app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();


// Инициализация внешних систем ПОСЛЕ Build(), НО ДО UseEndpoints

// Используем созданный ранее сервис вместо создания scope вручную

await app.Services.GetRequiredService<IEventHubInitializer>().InitializeAsync();


// 5. Точки входа (Endpoint Mapping)

app.MapHub<TradingDataHub>("/tradingDataHub"); // SignalR всегда перед MapGroup/Routes


app.MapGroup("/api/trading")

    .WithTags("Trading Data")

    .MapTradingEndpoints(); // Весь зоопарк MapGet переезжает сюда (см. ниже)


app.MapGroup("/api/debug").MapDebugEndpoints();

app.MapControllers();

app.MapRazorPages();


// 6. Startup/Lifecycle Logic (Без Thread.Sleep!)

app.Lifetime.ApplicationStarted.Register(() =>

{

    var logger = app.Services.GetRequiredService<ILogger<Program>>();

    var config = app.Services.GetRequiredService<IConfiguration>();

    

    // Получаем конфиги опциями

    var fileConfig = config.GetSection("FileOutput").Get<FileOutputConfig>()!;

    var displayConfig = config.GetSection("Display").Get<DisplayConfig>()!;

    

    if (fileConfig.EnableFileOutput)

    {

        WriteStartupInfoToFile(app, fileConfig); // Вынесено в private static метод

    }

    

    PrintAsciiBanner(logger, displayConfig); // Красивая печать без Sleep

});


app.Run();


// ========== ЛОКАЛЬНЫЕ ХЕЛПЕРЫ КОРНЯ (если они очень нужны) ==========

static void WriteStartupInfoToFile(WebApplication app, FileOutputConfig config) { /* Логика записи в файл */ }

static void PrintAsciiBanner(ILogger logger, DisplayConfig config) { /* Печать таблицы URL */ }

Шаг 4. Куда делись все MapGet?

Они переехали в отдельные файлы конечных точек (Minimal API Endpoints). Это стандартная практика Microsoft для масштабирования Minimal API.


Файл: /Presentation/TradingEndpoints.cs


csharp

Копировать

public static class TradingEndpoints

{

    public static RouteGroupBuilder MapTradingEndpoints(this IEndpointRouteBuilder routes)

    {

        var group = routes.MapGroup("/trading");


        group.MapGet("/summaries", GetSummaries);

        group.MapGet("/trades", GetTrades);

        group.MapGet("/deals", GetDeals);

        group.MapGet("/orders", GetOrders);

        group.MapGet("/logs", GetLogs);

        group.MapGet("/status", GetStatus);

        group.MapGet("/diagnostic", GetDiagnostic);


        return group;

    }


    // Реализации методов (GetSummaries, GetTrades...) находятся тут же внизу файла

    // или раскиданы по папкам Features/

    private static IResult GetSummaries(IInMemoryTradingDatabase db) => Results.Ok(db.GetSummaries());

    // ...

}

Что изменилось для вашего коллеги:

Поиск настроек: Нужно изменить уровень логов? Он идет в начало файла. Нужна новая зависимость? Он ищет её в файлах *DI.cs.

Безопасность: Убран Thread.Sleep. Приложение стартует мгновенно. Информация пишется в фоновом режиме.

Масштабируемость: Чтобы добавить новый эндпоинт /api/trading/history, коллега откроет TradingEndpoints.cs, а не будет скроллить 500 строк корня.

Порядок выполнения: Глядя на Program.cs, сразу видно жизненный цикл: Сначала строим контейнер -> Затем настраиваем пайплайн -> Затем инициализируем внешние связи -> Затем маппим маршруты -> Запускаемся.

Этот подход полностью соответствует философии Aspire: инфраструктурные вещи (Redis, Kestrel) скрыты, а ваша доменная сложность разложена по понятным полочкам согласно слоям ответственности.






Вопрос

Ваш коллега абсолютно прав, и его анализ бьет в самую суть проблемы современного .NET-разработки. То, что вы описали, в архитектуре называется Anti-Pattern "God Object" (Божественный объект) или "Composition Root Bloat" (Раздувание корня композиции).

Когда Program.cs превращается в свалку конфигураций, бизнес-логики, try-catch блоков и Thread.Sleep, проект становится неподдерживаемым. В экосистеме .NET Aspire, где оркестрация, HealthChecks и OpenTelemetry играют ключевую роль, такой подход просто убивает приложение при деплое (Aspire решит, что контейнер "завис" из-за Thread.Sleep и убьет его).

Ниже я поделюсь своим архитектурным каноном, который я использую в крупных Enterprise-проектах и микросервисах на .NET 8/9 + Aspire.


Моя философия: Program.cs — это "Оглавление", а не "Книга"

В больших проектах Program.cs должен выполнять роль Манифеста (Composition Root). Разработчик, открывший его, должен за 5 секунд понять:

Какие слои есть в приложении.

Какие внешние интеграции подключены.

Как выстроен HTTP-конвейер (Middleware).

Золотое правило: В Program.cs запрещено писать любую логику, циклы, условия if/else (кроме проверки Environment), чтение файлов и ручную инициализацию синглтонов.

Эталонная структура Program.cs (Мой канон)

Вот как выглядит Program.cs в моих крупных проектах. Он занимает от 15 до 30 строк.

using TradingPlatform.Infrastructure;

using TradingPlatform.Application;

using TradingPlatform.Presentation;

var builder = WebApplication.CreateBuilder(args);


// 1. Aspire & Observability (Логирование, OpenTelemetry, HealthChecks, Resilience)

// Вся магия логгирования и метрик уходит в проект ServiceDefaults

builder.AddServiceDefaults(); 


// 2. Слои приложения (Строгий порядок: Ядро -> Инфраструктура -> Презентация)

builder.Services.AddDomainCore();

builder.Services.AddApplicationServices();

builder.Services.AddInfrastructure(builder.Configuration);

builder.Services.AddPresentation(); // Minimal APIs, SignalR, Controllers


var app = builder.Build();


// 3. Middleware Pipeline (Обработка ошибок -> Роутинг -> Безопасность)

app.UseDefaultExceptionHandler(); // Кастомный middleware из ServiceDefaults

app.UseRouting();

app.UseAuthentication();

app.UseAuthorization();


// 4. Endpoint Mapping (Группировка маршрутов)

app.MapTradingPlatformEndpoints(); // Все Minimal API и SignalR хабы здесь

app.MapDefaultEndpoints();         // Aspire HealthChecks (/health, /alive)


app.Run();

Почему это работает?

builder.AddServiceDefaults(): В .NET Aspire создается отдельный проект ServiceDefaults. Именно туда уносится весь ваш код настройки Logging.AddSimpleConsole, AddFilter, AddOpenTelemetry. Корневой файл не должен знать, в каком формате пишутся логи.

Методы-расширения (AddInfrastructure, etc.): Каждый слой имеет свой файл DependencyInjection.cs.

Отсутствие try-catch: Если инфраструктура не может подняться (например, нет связи с EventHub), приложение должно упасть на старте (Fail Fast). Aspire увидит краш контейнера и перезапустит его. Скрывать ошибки старта в try-catch внутри Program.cs — это зло.

3. Решение ваших "болей" из оригинального кода

Ваш оригинальный код содержал три критические проблемы. Вот как они решаются в каноничной архитектуре.

Боль №1: Thread.Sleep и вывод баннеров при старте

Проблема: Thread.Sleep блокирует поток хоста. Aspire не может получить ответ от /health, считает сервис мертвым и перезагружает его.

Решение: Использование Startup Tasks (IHostedService).

Вся логика, которая должна выполниться один раз при старте (запись файлов, печать баннеров, инициализация EventHub), выносится в фоновый сервис.

csharp

// Файл: Application/StartupTasks/AppStartupBannerService.cs

public class AppStartupBannerService(

    IOptions<DisplayConfig> displayConfig, 

    IHostApplicationLifetime lifetime,

    ILogger<AppStartupBannerService> logger) : IHostedService

{

    public Task StartAsync(CancellationToken cancellationToken)

    {

        // Регистрируем колбэк на момент, когда сервер УЖЕ поднял порты и готов принимать трафик

        lifetime.ApplicationStarted.Register(() => 

        {

            // Печать баннера, запись файлов (без Thread.Sleep!)

            logger.LogInformation("🚀 Система запущена. Порты открыты.");

            // File.WriteAllText(...)

        });

        return Task.CompletedTask;

    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

}

Регистрация: services.AddHostedService<AppStartupBannerService>(); в слое Application.

Боль №2: Регистрация статических ключей EventHub (Scope в Program.cs)

Проблема: Создание scope прямо в Program.cs для инициализации базы или шины — это антипаттерн.

Решение: IHostedService или IStartupFilter (в .NET 8+ лучше IHostedService).


// Файл: Infrastructure/EventHub/EventHubInitializerService.cs

public class EventHubInitializerService(

    IDynamicSubscriptionManager subscriptionManager,

    ILogger<EventHubInitializerService> logger) : IHostedService

{

    public Task StartAsync(CancellationToken cancellationToken)

    {

        logger.LogInformation("🔑 Регистрация ключей EventHub...");

        subscriptionManager.RegisterStaticKey<TradeExecutedEvent>("trade:executed");

        // ... остальные ключи

        return Task.CompletedTask;

    }

    // ...


}

Этот сервис гарантированно выполнится до того, как начнут работать ваши QuotesGeneratorService и StrategiesExecutionService, если правильно настроить порядок (или использовать зависимости между HostedService).

Боль №3: Зоопарк app.MapGet(...) (500 строк路由)

Проблема: Смешивание роутов, логики и SQL-запросов в корне.

Решение: Паттерн Endpoint Groups (или использование библиотек вроде Carter / FastEndpoints).

// Файл: Presentation/Endpoints/TradingEndpoints.cs

public static class TradingEndpoints

{

    public static IEndpointRouteBuilder MapTradingEndpoints(this IEndpointRouteBuilder app)

    {

        var group = app.MapGroup("/api/trading")

                       .WithTags("Trading")

                       .RequireAuthorization(); // Глобальная защита для группы


Копировать

    group.MapGet("/summaries", GetSummaries);

    group.MapGet("/deals", GetDeals);

    // ...

    return app;

}


// Логика вынесена в статические методы или отдельные классы-обработчики

private static IResult GetSummaries(IInMemoryTradingDatabase db) 

    => Results.Ok(db.GetSummaries());

}

В Program.cs остается только одна строчка: app.MapTradingEndpoints();.

4. Структура папок (Манифест проекта)


Чтобы эта система работала, структура Solution должна быть жесткой:

src/

├── TradingPlatform.Domain/          # Сущности, события (EventHub events)

├── TradingPlatform.Application/       # UseCases, Interfaces, HostedServices (StartupTasks)

│   └── DependencyInjection.cs         # Метод AddApplicationServices()

├── TradingPlatform.Infrastructure/    # InMemoryDB, EventHub, FileWriters

│   └── DependencyInjection.cs         # Метод AddInfrastructure()

├── TradingPlatform.Api/               # Точка входа (Minimal API)

│   ├── Program.cs                     # 20 строк кода!

│   ├── Endpoints/                     # Папка с MapGroup расширениями

│   └── appsettings.json

└── TradingPlatform.ServiceDefaults/   # Aspire: Logging, OpenTelemetry, Resilience

Правила (Манифест), которые я требую от команды на Code Review

Если я вижу нарушение этих правил в Program.cs, я отправляю PR на доработку:

Правило "Никаких Ньютонов" (No new keyword): В Program.cs нельзя создавать экземпляры классов через new (кроме WebApplication.CreateBuilder). Все должно резолвиться через DI.

Правило "Чистого Конфига": Никаких configuration.GetSection("...").Get<MyConfig>() в корне. Только builder.Services.Configure<MyConfig>(builder.Configuration.GetSection("...")). Классы должны получать IOptions<MyConfig> через конструктор.

Правило "Fail Fast": Никаких try-catch при регистрации сервисов. Если InMemoryDatabase не может инициализироваться — приложение не должно стартовать.

Правило "Асинхронного старта": Запрет на Thread.Sleep и блокирующие .Result / .Wait(). Все, что требует времени (подключение к Redis, EventHub, чтение файлов), должно жить в IHostedService.

Правило "Скрытой Инфраструктуры": Настройка CORS, Swagger, Authentication, Serilog/Console должна быть упакована в методы-расширения (например, builder.Services.AddSwaggerDocumentation()).

Резюме

Ваш коллега мыслит в абсолютно правильном, зрелом архитектурном ключе. То, что вы называете "самодеятельностью", возникает из-за того, что Microsoft дала разработчикам слишком много свободы в Minimal APIs, не предоставив жестких рамок (как это было в старом Startup.cs с его ConfigureServices и Configure).

Переход к Layered DI (Слоистому DI), Endpoint Grouping и Startup Tasks (IHostedService) — это не просто "красивый код". Это единственный способ заставить .NET Aspire, HealthChecks и контейнеризацию работать стабильно, а новых разработчиков — не увольняться после первого взгляда на Program.cs.



Ответ

Ответ


Ваш коллега абсолютно прав, и его анализ бьет в самую суть проблемы современной .NET-разработки. То, что вы описали — это классический антипаттерн «God Object» (Божественный объект) или «Composition Root Bloat» (Раздувание корня композиции).


Когда Program.cs превращается в свалку конфигураций, бизнес-логики, блоков try-catch и вызовов Thread.Sleep, проект становится неподдерживаемым. В экосистеме Aspire такой подход просто убивает приложение при деплое: оркестратор решит, что контейнер завис из-за блокировки потока старта, и перезапустит его.


Ниже представлен архитектурный канон, который используется в крупных Enterprise-проектах на базе .NET 8/9 + Aspire.


1. Философия: Program.cs — это «Оглавление», а не «Книга»

В больших проектах корневой файл выполняет роль манифеста (Composition Root). Разработчик должен за 5 секунд понять:


Какие слои есть в приложении.

Какие внешние интеграции подключены.

Как выстроен HTTP-конвейер (Middleware).

Золотое правило: В Program.cs запрещено писать любую логику, циклы, условия if/else (кроме проверки окружения), чтение файлов и ручную инициализацию синглтонов.


2. Эталонная структура Program.cs (Канон)

Вот как выглядит типовой корень крупного проекта. Он занимает от 15 до 30 строк.


csharp

Копировать

using TradingPlatform.Infrastructure;

using TradingPlatform.Application;

using TradingPlatform.Presentation;


var builder = WebApplication.CreateBuilder(args);


// 1. Aspire & Observability 

// Вся магия логгирования уходит в ServiceDefaults

builder.AddServiceDefaults(); 


// 2. Слои приложения (Строгий порядок: Ядро -> Инфраструктура -> Презентация)

builder.Services.AddDomainCore();

builder.Services.AddApplicationServices();

builder.Services.AddInfrastructure(builder.Configuration);

builder.Services.AddPresentation(); // Minimal APIs, SignalR


var app = builder.Build();


// 3. Middleware Pipeline (Обработка ошибок -> Роутинг -> Безопасность)

app.UseDefaultExceptionHandler(); 

app.UseRouting();

app.UseAuthentication();

app.UseAuthorization();


// 4. Endpoint Mapping (Группировка маршрутов)

app.MapTradingPlatformEndpoints(); 

app.MapDefaultEndpoints();         // HealthChecks (/health, /alive)


app.Run();

Почему это работает?


AddServiceDefaults(): Весь ваш код настройки Logging.AddSimpleConsole, фильтров и OpenTelemetry живет в отдельном проекте ServiceDefaults.

Методы-расширения (AddInfrastructure): Каждый слой имеет свой файл DependencyInjection.cs.

Отсутствие try-catch: Если инфраструктура не может подняться (например, нет связи с EventHub), приложение должно упасть на старте (Fail Fast). Aspire увидит краш контейнера и корректно обработает его падение.

3. Решение ваших «болей» из оригинального кода

Проблема Антипаттерн Каноничное решение

Thread.Sleep и вывод баннеров Блокирует поток хоста. Aspire убьет сервис по таймауту health check. IHostedService. Логика записи файлов и печати консоли переносится в фоновый сервис AppStartupBannerService.

Регистрация ключей EventHub через scope Создание скоупа вручную внутри корня ломает жизненный цикл DI. IHostedService. Создается EventHubInitializerService, который регистрирует ключи в методе StartAsync.

Зоопарк app.MapGet (500 строк) Смешивание роутов, логики запросов и DTO в одном файле. Endpoint Groups. Все эндпоинты переезжают в статический класс TradingEndpoints.cs с методом расширения MapTradingEndpoints().

Ручная настройка логгеров Дублирование конфигурации фильтрации для каждого сервиса. Вынос в ServiceDefaults или создание общего метода builder.ConfigureStructuredLogging().

4. Структура папок (Манифест проекта)

Чтобы эта система работала, структура решения должна быть жесткой:


text

Копировать

src/

├── TradingPlatform.Domain/          # Сущности, события (TradeExecutedEvent)

├── TradingPlatform.Application/     # UseCases, Interfaces, HostedServices

│   └── DependencyInjection.cs       

├── TradingPlatform.Infrastructure/  # InMemoryDB, EventHub, FileWriters

│   └── DependencyInjection.cs       

├── TradingPlatform.Api/             # Точка входа (Minimal API)

│   ├── Program.cs                   # Только 20 строк!

│   ├── Endpoints/                   # Папка с MapGroup расширениями

│   └── appsettings.json

└── TradingPlatform.ServiceDefaults/ # Aspire: Logging, OTEL, Resilience

5. Правила Code Review (Архитектурный договор)

Если я вижу нарушение этих правил в Program.cs, Pull Request отправляется на доработку:


Правило «Никаких Ньютонов»: В Program.cs нельзя создавать экземпляры классов через new (кроме самого билдера). Всё резолвится через DI.

Правило «Чистого Конфига»: Никаких configuration.GetSection("X").Get<MyConfig>() в корне. Используйте builder.Services.Configure<MyConfig>(...) и передавайте IOptions<MyConfig> в конструкторы.

Правило «Fail Fast»: Никаких try-catch вокруг регистрации сервисов. Ошибка подключения к БД — повод остановить релиз-пайплайн.

Правило «Асинхронного старта»: Запрет на Thread.Sleep, .Result и .Wait(). Любая задержка (подключение к Redis, шинам) живет только в IHostedService.

Правило «Скрытой Инфраструктуры»: Настройка CORS, Swagger, Authentication упакована в методы-расширения (например, builder.Services.AddSwaggerDocumentation()).

Ваш коллега мыслит в зрелом архитектурном ключе. Переход к Layered DI, Endpoint Grouping и Startup Tasks — это единственный способ заставить ASP.NET Core работать стабильно под управлением Aspire и Kubernetes, а новым разработчикам — понимать систему без многодневного изучения одного файла.






Вопрос

Моё мнение: проблема реальна, решения — зрелые, но внедрение требует дисциплины и контекста.


Коллеги абсолютно правы: современный Program.cs в .NET (особенно с минимальными API) превратился в «свалку» — место, где смешиваются регистрации DI, конфигурация middleware, бизнес-логика, инициализация, запись в файлы и даже Thread.Sleep. Это антипаттерн «God Object», который убивает поддерживаемость, тестируемость и даже стабильность в оркестраторах вроде Aspire/Kubernetes.


Предложенные каноны (слоистая регистрация, вынос в методы расширения, IHostedService для стартовых задач, группировка эндпоинтов) — это не просто «красивый код», а архитектурная необходимость для проектов, которые живут дольше двух недель. Однако хочу выделить несколько нюансов, которые часто упускают, когда слепо копируют эти паттерны.


Слоистая регистрация — правильно, но не догма

Разбиение на AddDomainCore(), AddApplication(), AddInfrastructure() — отличный способ явно выразить архитектуру (чистая архитектура, onion, hexagonal). Это решает проблему «где искать настройку SMTP?» — ответ всегда в Infrastructure/DI.cs.

Однако:


Строгое следование слоям может усложнить внедрение сквозных функций (cross-cutting concerns) — например, логгирование, валидация, кеширование. Их часто приходится регистрировать в нескольких слоях или создавать отдельный AddShared().


В небольших проектах (микросервис с 3–4 сущностями) избыточное разделение на 4–5 сборок может быть оверкиллом. Я рекомендую минимально необходимое число слоёв, но с чёткими границами. Например, можно объединить Domain и Application в одну сборку, если они тесно связаны, но Infrastructure и Presentation всегда выделять.


Вынос логики в IHostedService — спасение, но с оговорками

Инициализация EventHub, запись баннеров, предварительный прогрев кеша — всё это обязательно должно жить в фоновых сервисах, а не в корне Program.cs. Это решает проблему блокирующих операций (Thread.Sleep, .Result) и позволяет Aspire/K8s корректно проверять health-проверки.

Нюансы:


Порядок запуска нескольких IHostedService не гарантирован, если не использовать DependsOn или явные зависимости через конструктор. В вашем примере TradingLogService должен стартовать после StrategiesExecutionService, но если оба зарегистрированы как IHostedService, порядок их StartAsync определяется порядком регистрации. Однако это ненадёжно в случае параллельного запуска. Решение: использовать IHostedLifecycleService (в .NET 8+) или внедрить IServiceProvider и запускать инициализацию вручную через ApplicationStarted (но тогда теряется преимущество IHostedService). Лучше использовать очередь инициализаторов или библиотеку вроде DotNetCore.CAP для упорядоченной инициализации.


Если IHostedService выбрасывает исключение, приложение упадёт (Fail Fast) — это хорошо. Но если инициализация должна повториться (например, подключение к БД) — стоит добавить политику повторных попыток внутри сервиса, а не полагаться на перезапуск контейнера.


Группировка эндпоинтов — обязательна, но не забывайте про документацию

Вынос всех MapGet в отдельные классы (TradingEndpoints) — здравая идея. Это делает Program.cs лаконичным, а эндпоинты — тестируемыми и группируемыми (например, по версиям API).

Однако:


Когда эндпоинтов становится сотни, статические методы в одном файле тоже превращаются в «свалку». Я рекомендую делить по фичам (Feature Slices) — каждая фича (например, Orders, Trades, Reports) имеет свой собственный класс эндпоинтов, а внутри — несколько методов-обработчиков.


Для автоматической документации (Swagger/OpenAPI) удобно использовать атрибуты [Endpoint] или библиотеки вроде FastEndpoints, которые дают строгую типизацию и встроенную валидацию. Но если проект уже на Minimal API, группировка через RouteGroupBuilder — достаточный компромисс.


Конфигурация и опции — строго через IOptions

Совет «никакого GetSection().Get<>() в корне» — абсолютно правильный. Все настройки должны быть зарегистрированы через services.Configure<T>, а классы получать IOptions<T> или IOptionsSnapshot<T>. Это позволяет:

Легко переопределять настройки в тестах.


Использовать валидацию через DataAnnotations или FluentValidation.


Поддерживать перезагрузку конфигурации (IOptionsSnapshot).


Исключение: иногда нужно прочитать настройку один раз при старте, чтобы передать её в какой-то статический класс (например, для инициализации логгера). Но даже тогда лучше использовать фабрику, которая получает IConfiguration внутри метода расширения.


Логгирование — скрыть в ServiceDefaults

У вас в оригинале куча AddFilter и настроек консольного логгера. В Aspire-проектах это действительно выносят в проект ServiceDefaults. Это правильно: корневой Program.cs не должен знать, какой формат логов (json, plain) и какие категории фильтровать. Однако я бы советовал оставить в корне минимальную настройку, например, builder.Logging.AddConsole() и всё остальное — в метод расширения AddObservability(), который вызывается первым.


Проблема «скрытых зависимостей» — статические ключи EventHub

В вашем примере регистрация статических ключей выполняется в скоупе внутри Program.cs. Это опасно:


Создание скоупа в корне — антипаттерн, потому что нарушает жизненный цикл DI.


Если регистрация упадёт, приложение продолжит старт (из-за try-catch), что приведёт к неработающему EventHub.


Решение через IHostedService или IStartupFilter — правильное. Но я бы предложил сделать это частью конструктора EventHubTickerManager или использовать интерфейс IValidateOptions для проверки, что все ключи зарегистрированы до старта приложения. Например, можно создать синглтон EventHubKeyRegistry, который заполняется при регистрации сервисов, а потом проверяется в IHostedService.


Важное: разделение на «публикаторы» и «подписчики»

В вашем коде порядок фоновых сервисов жёстко зафиксирован (сначала генераторы, потом подписчики). Это работает, но хрупко. Лучше использовать EventHub сам по себе как шину, где порядок подписки не важен, а важна обработка сообщений. Если же нужен строгий порядок (например, логирование должно происходить до мониторинга), стоит реализовать цепочку обработчиков внутри одного подписчика или использовать MediatR с поведением.

В общем, IHostedService — это хорошо, но не злоупотребляйте порядком; делайте сервисы как можно более независимыми.


Что остаётся в Program.cs? — «Оглавление»

Согласен с коллегами: идеальный Program.cs должен быть декларативным и занимать 15–30 строк. В нём:

Вызов builder.AddServiceDefaults() (если Aspire).


Последовательный вызов методов расширения (слои).


Построение app.


Настройка middleware (Use...).


Маппинг групп эндпоинтов (MapGroup).


Регистрация колбэков жизненного цикла (но без сложной логики).


Всё остальное — в других файлах.


Но есть и «тёмная сторона» этого подхода

Усложняется отладка. Когда все сервисы инициализируются в IHostedService, трассировка ошибок старта становится менее очевидной (исключения могут быть проглочены, если не обработать Task). Нужно явно логировать все исключения.

Сложность с тестированием. Тестировать методы расширения сложнее, чем тестировать сам Program.cs (но это решается модульными тестами на самих сервисах).


Избыточная абстракция. Иногда проще оставить несколько строк в корне, чем создавать целый класс для одной строчки кода. Баланс — ключевой.


Мой итоговый вердикт

Коллеги правы на 99%. Их рекомендации — это золотой стандарт для поддерживаемых .NET-решений, особенно в контексте Aspire, где важны health checks, оркестрация и наблюдаемость.


Но я бы добавил два важных дополнения:


Внедряйте эти правила постепенно, через code review и соглашения команды. Не пытайтесь переписать всё за один день — это приведёт к ошибкам.


Не бойтесь нарушать правила, если это упрощает код в конкретном, простом микросервисе. Например, в сервисе-адаптере с 3 эндпоинтами можно оставить их в Program.cs, если они тривиальны.


Главное — единообразие в рамках проекта/компании. Если все пишут по-разному, никакие паттерны не спасут. Создайте шаблон решения (solution template) на основе этих канонов и используйте его для всех новых проектов. Это и есть та самая «типовая структура», о которой говорил ваш коллега.


В заключение — краткий чек-лист для Program.cs

□ Не более 30 строк кода.

□ Никаких new, Thread.Sleep, Task.Wait.

□ Никаких try-catch в корне.

□ Все регистрации — через методы расширения по слоям.

□ Все эндпоинты — в MapGroup с вынесением в отдельные классы.

□ Все стартовые задачи — в IHostedService.

□ Конфигурация — через IOptions.

□ Логирование и метрики — в ServiceDefaults (или отдельном методе).

Это превратит хаос в предсказуемую, масштабируемую и понятную архитектуру. А значит, ваш коллега сможет прийти в проект через полгода и сразу понять, где что искать. Именно этого мы все и хотим.


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


Многие команды впадают в крайности: либо оставляют «свалку» из 500 строк кода (как было у вас), либо доводят рефакторинг до абсурда, создавая по пять классов для регистрации одной строки DI.


Ваш вердикт — это зрелая позиция архитектора. Позвольте разобрать те самые нюансы, которые вы выделили, так как именно они отличают работающий Enterprise-проект от красивой картинки в документации Microsoft.


1. Слоистая регистрация vs Контекст задачи

Вы правы: строгий Onion/Hexagonal подход может стать оверкиллом для микросервиса на три эндпоинта. Однако главная ценность слоев здесь не в количестве сборок, а в порядке инициализации.


В вашем старом коде порядок был хрупким комментарием («Сначала сервисы-публикаторы...»).

В каноничном Program.cs этот порядок становится явным контрактом:

csharp

Копировать

builder.Services.AddDomain(); // Интерфейсы - всегда первыми

builder.Services.AddApplication(); // Логика

builder.Services.AddInfrastructure(); // Реализации + IHostedService

Если фоновый сервис зависит от репозитория, компилятор или рантайм сразу скажет об ошибке. Если же всё свалено в кучу, такая ошибка всплывет только при падении продакшна.


2. Порядок IHostedService — самая частая ловушка .NET

Это критический момент, который многие упускают. Как вы верно заметили, если зарегистрировать два сервиса подряд:


csharp

Копировать

services.AddHostedService<LogWriter>();

services.AddHostedService<DataGenerator>();

.NET не гарантирует, что DataGenerator.StartAsync() вызовется раньше LogWriter.StartAsync(). Они стартуют параллельно. Если генератор попытается залогировать событие до того, как логгер проинициализировал свои соединения, возникнет гонка (race condition).


Как решается эта проблема без самодеятельности:


Явные зависимости через конструктор. Пусть DataGenerator принимает ILogWriter в конструкторе. DI-контейнер заставит фреймворк создать LogWriter первым.

Библиотеки координации (Silos). Для сложных сценариев используют библиотеки вроде Coravel Pro или самодельный реестр задач, где можно указать DependsOn.

IHostedLifecycleService (.NET 8+). Дает более тонкий контроль над стадиями старта (Starting, Started), но требует аккуратной обработки исключений.

3. Проблема скрытых зависимостей (EventHub Keys)

Ваша интуиция насчет антипаттерна со скоупом в корне бесценна. Создание using var scope = app.Services.CreateScope() внутри Program.cs ради настройки шины ломает принцип Fail Fast. Приложение стартует, делает вид, что работает, но EventHub фактически мертв.


Решение: Регистрация ключей должна быть частью жизненного цикла самого менеджера тикеров (EventHubTickerManager). Он должен проверять наличие ключей в своем конструкторе или методе StartAsync и выбрасывать исключение, если шина не сконфигурирована. Это переносит ответственность за консистентность данных внутрь бизнес-логики, а не оставляет её в инфраструктурном скрипте запуска.


4. Тёмная сторона абстракций

Вы очень точно подметили риски чрезмерного дробления:


Отладка: Когда логика размазана по десяти файлам DependencyInjection.cs, поиск причины падения стека вызова усложняется. Решение: обязательный .ValidateScopes() и .ValidateOnBuild() перед app.Build(). Это заставляет контейнер проверить все связи еще до попытки запустить первый хостед-сервис.

Избыточность: Писать отдельный файл расширения ради одного AddSingleton<ILogger>() бессмысленно. Правило простое: выносите в методы расширения только то, что используется минимум в двух местах проекта или имеет сложную внутреннюю настройку (например, настройка Polly Resilience handlers).

Итог: Шаблон решения (Solution Template)

Главный вывод вашего анализа — необходимость дисциплины. Паттерны сами по себе ничего не значат, если каждый разработчик интерпретирует их по-своему.


Единственный способ победить хаос в Program.cs — это внутренний NuGet-пакет-шаблон компании. 

Когда новый проект создается командой dotnet new trading-service --name MyOrders, он уже должен содержать:


Жесткую структуру папок.

Готовые файлы Infrastructure/DI.cs и Presentation/Endpoints.cs.

Базовый Program.cs на 20 строк, где закомментированы места для вставки специфичной логики.

Настроенный Aspire AppHost.

Тогда ваш коллега придет в проект, увидит знакомую структуру и потратит время на написание торговых алгоритмов, а не на археологические раскопки в поисках того, где регистрируется фабрика подключения к Redis.


Вопрос

Анализ проблемы и рекомендации по организации Program.cs

Основная проблема

Program.cs превратился в свалку кода из-за смешения:


Регистрации сервисов

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

Бизнес-логики

Инициализации компонентов

Операций ввода-вывода

Ключевые решения

Слоистая архитектура

Разделение на доменный, прикладной и инфраструктурный уровни

Использование методов-расширений для регистрации

Пример структуры:

csharp

builder.Services.AddDomain();

builder.Services.AddApplication();

builder.Services.AddInfrastructure();

Управление зависимостями

Использование IHostedService для фоновых задач

Явное определение порядка инициализации через:

Зависимости в конструкторах

IHostedLifecycleService

Библиотеки координации

Организация эндпоинтов

Группировка по фичам

Использование RouteGroupBuilder

Вынесение логики в отдельные классы

Практические рекомендации

Ограничения Program.cs

Не более 30 строк кода

Отсутствие прямого создания объектов

Минимум логики, максимум декларативности

Правила внедрения

Конфигурация через IOptions

Логирование в отдельном слое

Наблюдаемость вынесена в ServiceDefaults

Стартовые задачи в IHostedService

Типичные ошибки и их решения

Проблемы порядка инициализации

Решение: явное определение зависимостей

Использование паттерна зависимости через конструктор

Скрытые зависимости

Вынесение регистрации в конструкторы сервисов

Валидация конфигурации при старте

Использование IValidateOptions

Избыточная абстракция

Баланс между чистотой кода и простотой

Вынесение только значимой логики

Сохранение читаемости

Рекомендации по внедрению

Постепенное внедрение

Через code review

Создание шаблонов проектов

Документирование стандартов

Контроль качества

Чек-листы для code review

Автоматические проверки

Стандарты именования

Итоговый чек-лист

Структура Program.cs:


Вызов базовых настроек

Регистрация слоев

Настройка middleware

Маппинг эндпоинтов

Запуск приложения

Запрещено в Program.cs:


new-операторы (кроме создания билдера)

Thread.Sleep

Task.Wait

try-catch блоки

Прямая работа с конфигурацией

Обязательно:


Использование методов расширения

Группировка эндпоинтов

Вынесение логики в сервисы

Конфигурация через IOptions

Такой подход обеспечит:


Поддерживаемость кода

Предсказуемость поведения

Легкость масштабирования

Простоту онбординга новых разработчиков


Вы составили отличный, структурированный чек-лист. Это именно тот документ, который должен висеть на стене (или в Wiki) команды .NET-разработки. Вы свели наш диалог к сухой выжимке архитектурных правил.


Чтобы этот список стал рабочим инструментом для вашей команды, я оформил его как «Архитектурный стандарт Program.cs». Его можно использовать прямо в качестве шаблона для Code Review или включить в Definition of Done задачи.


Ниже приведены пояснения к ключевым пунктам вашего списка с точки зрения реальной разработки и Aspire.


1. Управление порядком: Тонкий момент IHostedService

В вашем списке указано решение через зависимости в конструкторах. Важно добавить технический нюанс:


Если BackgroundService A зависит от BackgroundService B только потому, что логически А должен стартовать раньше, конструктор не поможет — DI создаст оба объекта одновременно.

Порядок вызова их методов StartAsync() будет зависеть от порядка регистрации (services.AddHostedService<A>(); services.AddHostedService<B>();), но это хрупко.

Решение: Используйте явную сигнализацию готовности. Сервис B может выставлять флаг _isReady = true, а сервис A в цикле ожидания проверять этот статус перед началом работы, либо используйте библиотеки координации задач.

2. Скрытые зависимости и Fail Fast

Вы упомянули использование IValidateOptions. Это критически важный инструмент для предотвращения «тихих отказов».

Пример из вашего старого кода: регистрация ключей EventHub внутри блока try-catch. 


Проблема: Если ключ не зарегистрируется, приложение продолжит работу, трейсы OpenTelemetry будут уходить в никуда, а стратегии начнут падать по таймауту спустя час после запуска.

Решение: Вместо try-catch при старте сделайте регистрацию частью конфигурации опций или проверки сервиса. Если шина недоступна — метод расширения .AddInfrastructure() выбрасывает исключение, и контейнер падает сразу (Fail Fast).

3. Баланс абстракции (Избыточность vs Хаос)

Правило «не более 30 строк» часто приводит к созданию мусорных файлов вида LoggingExtensions.cs ради одной строки builder.Services.AddSerilog().

Критерий вынесения в метод расширения:


Регистрация требует настройки нескольких взаимосвязанных пакетов (например, Polly + HttpClientFactory + Service Discovery). Пример: ваш блок фильтрации логов идеально ложится в один метод builder.ConfigurePlatformLogging().

Настройка повторяется в разных микросервисах компании.

Если же настройка уникальна и занимает одну строку — оставьте её в корне. Читаемость важнее догматической чистоты.

4. Конфигурация эндпоинтов (Feature Slices)

Группировка через MapGroup обязательна, но важно соблюдать иерархию версий.


csharp

Копировать

// Плохо: все API в куче

app.MapTradingEndpoints();


// Хорошо: четкое разделение ответственности

app.MapGroup("/api/v1")

   .WithTags("API v1")

   .MapV1Endpoints();


app.MapGroup("/api/v2") 

   .WithTags("API v2")

   .RequireAuthorization()

   .MapV2Endpoints();

Это спасает жизнь, когда нужно поддерживать обратную совместимость со старыми мобильными клиентами, пока фронтенд уже перешел на новую версию контракта.


Итоговый артефакт для внедрения

Ваш чек-лист идеален для автоматизации. Чтобы он работал без постоянного надзора архитектора, внедрите следующие инструменты контроля качества:


Solution Template (.template.config): Создайте внутренний NuGet-пакет-шаблон проекта. Новый микросервис должен создаваться командой dotnet new apibff -n Orders. В этом шаблоне Program.cs уже содержит правильную структуру, а пустые файлы слоев лежат в папках /Domain, /Application, /Infrastructure.

Анализаторы Roslyn (в файле Directory.Build.props): Добавьте правило, которое выдает предупреждение или ошибку, если длина файла Program.cs превышает определенный порог (например, 50–60 строк), или если там найдены запрещенные ключевые слова (Thread.Sleep, CreateScope).

xml

Копировать

<ItemGroup>

  <Analyzer Include="$(NuGetPackageDir)YourCompany.Analyzers\...\YourCompany.Analyzers.dll" />

</ItemGroup>

Чек-лист Pull Request: Автоматический бот (GitHub Actions / GitLab CI), который оставляет комментарий на PR, если видит изменения в Program.cs, напоминая проверить соблюдение стандартов.

Этот подход превращает архитектуру из предмета споров в жесткий контракт среды исполнения.


Комментариев нет:

Отправить комментарий