суббота, 19 сентября 2026 г.

Qwen, Aspire, Projects, Gateway, Http, Part 2

Qwen, Aspire, Projects, Gateway,  Http, Part 2

https://chat.qwen.ai/s/75eecb49-01e6-446f-8953-2f35f9d9d9b8?fev=0.2.91

Да. Это уже не просто идея, а настоящий рабочий скелет вашей архитектуры.
Самое важное: вы уже начали реализовывать не “просто набор проектов”, а именно ту концепцию, о которой мы говорили:
Web -> Gateway -> внутренние сервисы,
где Gateway — центральная точка входа, а AppHost — исполняемый закон топологии.

То есть это уже начало «Управляемой звезды» / Декларативной федерации.

Что уже хорошо

1. Правильная топология в Trading.AppHost/Program.cs

У вас уже есть ядро архитектурного закона:
var ordersApi = builder.AddProject<Projects.Trading_Api_Orders>("orders-api"); var marketDataApi = builder.AddProject<Projects.Trading_Api_MarketData>("market-data-api"); var gateway = builder.AddProject<Projects.Trading_Gateway>("gateway") .WithReference(ordersApi) .WithReference(marketDataApi); var web = builder.AddProject<Projects.Trading_Web>("web") .WithReference(gateway);

Это уже правильно:
  • web знает только gateway;
  • gateway знает про orders-api и market-data-api;
  • бизнес-сервисы не знают друг о друге;
  • фронтенд не может напрямую обратиться к orders-api или market-data-api, потому что у него нет на них ссылки.
Это и есть принцип:
Безопасность и архитектурная чистота через физическое отсутствие путей.
Для начала — отлично.

2. Trading.Gateway уже использует YARP

В Trading.Gateway/Program.cs:
builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) .AddServiceDiscoveryDestinationResolver();
---
Это очень правильно.
Вы не просто сделали отдельный веб-проект, а начали делать настоящий L7-роутер:
  • маршрутизация по путям /api/orders, /api/marketdata;
  • кластеры orders-cluster, marketdata-cluster;
  • интеграция с Aspire Service Discovery;
  • диагностический эндпоинт /debug/routes.
Это уже хороший фундамент для будущего CentralRouter.

3. Web уже ходит через Gateway

В Trading.Web/Program.cs:
builder.Services.AddHttpClient<TradingApiClient>(client => { client.BaseAddress = new Uri("http://gateway"); });
-------
А в TradingApiClient.cs запросы идут на:
/api/orders /api/marketdata/{symbol}
То есть фронтенд не знает реальные адреса бизнес-сервисов. Он знает только логическое имя:
gateway
Это правильно.

4. Уже есть диагностический эндпоинт

В Trading.Gateway/Program.cs:
app.MapGet("/debug/routes", ...)
Очень полезная вещь.
На раннем этапе это помогает видеть, что YARP реально загрузил маршруты и кластеры.

Что я бы поправил в первую очередь

1. Убрать фиксированный порт у Gateway, если он не обязателен

Сейчас:
var gateway = builder.AddProject<Projects.Trading_Gateway>("gateway") .WithHttpEndpoint(port: 5000, name: "http") ...
Для локальной разработки фиксированный порт не обязателен.
Лучше оставить Aspire самому назначать порт, если только вам не нужен конкретный адрес для внешнего доступа.
Можно упростить:
var gateway = builder.AddProject<Projects.Trading_Gateway>("gateway") .WithReference(ordersApi) .WithReference(marketDataApi);
Если нужно внешнее обращение именно к Gateway, тогда можно добавить:
.WithExternalHttpEndpoints()
Но для внутренней архитектуры достаточно WithReference.

2. В Trading.Gateway/appsettings.json лучше убрать завершающий слэш

Сейчас:
"Address": "http://orders-api/"
"Address": "http://orders-api"
"Address": "http://market-data-api"
С завершающим слэшем обычно все работает, но чище использовать адрес без последнего /.
То есть:
"Clusters": { "orders-cluster": { "Destinations": { "orders-destination": { "Address": "http://orders-api" } } }, "marketdata-cluster": { "Destinations": { "marketdata-destination": { "Address": "http://market-data-api" } } } }

3. Trading.ApiService пока лишний

У вас есть проект:
Trading.ApiService
Он подключен в Trading.AppHost.csproj, но в текущем Trading.AppHost/Program.cs не используется.
На данном этапе он только засоряет граф и может путать.
Я бы пока:
  • либо удалил его;
  • либо отключил из решения;
  • либо оставил отдельно под будущий MatchingEngine/TradingCore, если он для этого задумывался.
Но для текущего этапа web -> gateway -> orders/marketdata он не нужен.

4. Потокобезопасность в Trading.Api.Orders

Сейчас там:
var orders = new List<Order>();
Для демо нормально, но это не потокобезопасно.
Если два запроса одновременно добавят ордер, могут быть проблемы.
Для начала можно заменить на:
using System.Collections.Concurrent; var orders = new ConcurrentDictionary<Guid, Order>();
И эндпоинты:
app.MapGet("/api/orders", () => orders.Values.OrderBy(x => x.CreatedAt).ToList()); app.MapPost("/api/orders", (Order order) => { order.Id = Guid.NewGuid(); order.CreatedAt = DateTime.UtcNow; orders[order.Id] = order; return Results.Created($"/api/orders/{order.Id}", order); });
Это все еще in-memory, но уже безопаснее.

5. Вынести контракты в отдельный проект

Сейчас у вас есть дублирование моделей.
В Trading.Api.Orders:
record Order
В Trading.Web/TradingApiClient.cs:
public record Order
Это нормально для самого первого шага, но дальше лучше завести:
Trading.Contracts
или
Trading.Shared.Contracts
И туда положить:
public record OrderDto
{
    public Guid Id { get; set; }
    public string Symbol { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Quantity { get; set; }
    public string Side { get; set; } = "Buy";
    public DateTime CreatedAt { get; set; }
}
и
public record MarketDataDto( string Symbol, decimal Price, DateTime Timestamp );
Тогда:
  • Trading.Api.Orders использует контракты;
  • Trading.Api.MarketData использует контракты;
  • Trading.Web использует контракты;
  • Trading.Gateway использует контракты.
Это будет следующий важный шаг.

Что у вас сейчас уже получилось по факту

У вас уже есть:
Trading.Web | | HTTP через логическое имя "gateway" v Trading.Gateway | | YARP + Service Discovery v Trading.Api.Orders Trading.Api.MarketData
Это уже не спагетти.
Это уже централизованная точка входа.
И главное: эта топология описана в AppHost.
То есть вы уже начали превращать архитектурное соглашение из «договоренности» в физический закон среды.

Что сейчас лучше не делать

На этом этапе я бы не добавлял:
  • RabbitMQ;
  • Kafka;
  • MassTransit;
  • WolverineFx;
  • Redis как обязательную шину;
  • сложный Event Sourcing.
Сейчас важно довести до стабильности простой путь:
HTTP -> Gateway -> Backend Services
Когда этот путь станет ясным, наблюдаемым и удобным, тогда можно добавлять:
  • Outbox;
  • асинхронные команды;
  • Wolverine;
  • SignalR;
  • Matching Engine.
Но сначала надо закрепить базу.

Следующий логичный шаг

Шаг 1. Сделать Gateway не только прокси, но и первым “умным” BFF

Сейчас Gateway в основном проксирует:
/api/orders -> orders-api /api/marketdata -> market-data-api
Следующий шаг — добавить композиционный эндпоинт.
Например:
GET /api/trading/dashboard/{symbol}
Он должен возвращать:
{ "symbol": "AAPL", "marketData": { "price": 175.50, "timestamp": "2026-..." }, "orders": [ { "id": "...", "symbol": "AAPL", "side": "Buy", "quantity": 10, "price": 175.50 } ] }
То есть Gateway сделает два внутренних вызова:
GET http://market-data-api/api/marketdata/AAPL GET http://orders-api/api/orders
И склеит ответ.
Это уже будет настоящий Router/BFF.

Пример идеи для Trading.Gateway/Program.cs

Можно добавить два typed-client:
builder.Services.AddHttpClient("orders", client => { client.BaseAddress = new Uri("http://orders-api"); }); builder.Services.AddHttpClient("marketdata", client => { client.BaseAddress = new Uri("http://market-data-api"); });
И эндпоинт:
app.MapGet("/api/trading/dashboard/{symbol}", async ( string symbol, IHttpClientFactory httpClientFactory) => { var marketClient = httpClientFactory.CreateClient("marketdata"); var ordersClient = httpClientFactory.CreateClient("orders"); var marketTask = marketClient.GetFromJsonAsync<MarketDataDto>($"/api/marketdata/{symbol}"); var ordersTask = ordersClient.GetFromJsonAsync<List<OrderDto>>("/api/orders"); await Task.WhenAll(marketTask, ordersTask); var market = await marketTask; var allOrders = await ordersTask ?? new List<OrderDto>(); var filteredOrders = allOrders .Where(x => string.Equals(x.Symbol, symbol, StringComparison.OrdinalIgnoreCase)) .ToList(); return Results.Ok(new { Symbol = symbol.ToUpper(), Market = market, Orders = filteredOrders }); });
то будет уже не просто маршрутизация, а агрегация данных.

Маленькие технические замечания

1. Опечатка в имени папки

У вас:
Trading.Api.MarketDat
Trading.Api.MarketData.csproj
Это не критично, но лучше переименовать, пока проект маленький.
Потом будет неприятно, если:
MarketDat MarketData MarketDat.Controllers MarketData.Services
начнут перемешиваться.

2. Старые WeatherForecast-файлы

Во многих проектах остались:
WeatherForecast.cs WeatherForecastController.cs /weatherforecast
Это стандартный шаблон.
Для чистоты их можно удалить.
Особенно в:
Trading.Api.Orders Trading.Api.MarketDat Trading.ApiService
Чтобы не было ощущения, что они участвуют в бизнес-логике.

3. В Trading.Web есть Redis OutputCaching, но Redis пока не используется

В Trading.Web.csproj:
<PackageReference Include="Aspire.StackExchange.Redis.OutputCaching" Version="13.4.6" />
Но в текущем Trading.AppHost/Program.cs Redis нет.
Если пока не используете, пакет можно убрать, чтобы не создавать ложное ощущение зависимости.

Что можно считать “готовым первым уровнем”

Я бы сказал, что первый уровень готов, если выполнены следующие пункты:

1. Все сервисы видны в Aspire Dashboard

Должны быть:
web gateway orders-api market-data-api
Без лишних сервисов.

2. Фронтенд работает через Gateway

Страница:
/trading
должна:
  • загрузить рыночные данные по AAPL;
  • создать ордер;
  • получить список ордеров.
И все это должно идти через:
gateway
а не напрямую в orders-api.

3. В Aspire Dashboard виден сквозной trace

Идеально, если видно цепочку:
Browser -> web -> gateway -> market-data-api
---
Browser -> web -> gateway -> orders-api
Это будет означать, что наблюдаемость уже работает.

4. /debug/routes показывает маршруты

Например:
orders-route-base orders-route marketdata-route-base marketdata-route
И кластеры:
orders-cluster marketdata-cluster

Итог

Да, вы абсолютно правы:
Здесь мы уже начинаем что-то делать.
И это не просто начало.
Это начало правильной архитектуры.
Уже есть три важнейшие вещи:
  1. Центральный Gateway
  2. Изоляция фронтенда от внутренних сервисов
  3. Декларативная топология через Aspire AppHost
То есть вы уже начали реализовывать вашу главную идею:
Сервисы не должны сами тянуться друг к другу. Они должны жить внутри управляемой среды, где связи являются законом, а не случайностью.
Следующий шаг я бы сделал такой:
Превратить Trading.Gateway из простого прокси в первый интеллектуальный BFF/Router.
То есть добавить хотя бы один композиционный эндпоинт, например:
GET /api/trading/dashboard/{symbol}
который объединит:
  • рыночные данные;
  • ордера;
  • возможно, статус позиции или риск-лимит позже.
Это будет уже настоящий шаг от «прокси» к «мозгу системы».

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

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