Практикум — сервис заказов на C#
Практикум, шаг 5 из 8. orders-api оркестрирует резерв в catalog-api. DTO — шаг 3.
Создание проекта
cd OrderDesk
dotnet new web -n orders-api -o orders-api
cd orders-api
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
appsettings.json:
{
"Catalog": {
"BaseUrl": "http://localhost:8100",
"ApiKey": "dev-catalog-key-change-me"
},
"Jwt": {
"Issuer": "orderdesk",
"Audience": "orderdesk-clients",
"SigningKey": "DEV_ONLY_32_CHAR_MINIMUM_SECRET!!"
}
}
HttpClient к каталогу
Services/CatalogClient.cs:
Код ITЗагрузка примера кода…
Регистрация в Program.cs:
builder.Services.AddHttpClient<CatalogClient>((sp, client) =>
{
var cfg = sp.GetRequiredService<IConfiguration>().GetSection("Catalog");
client.BaseAddress = new Uri(cfg["BaseUrl"]!);
client.DefaultRequestHeaders.Add("X-Api-Key", cfg["ApiKey"]);
client.Timeout = TimeSpan.FromSeconds(5);
});
Домен и EF Core
Models/Order.cs — статусы Draft, Reserved, Confirmed, Cancelled, Failed.
Data/OrdersDbContext.cs — таблицы Orders, OrderLines.
Services/OrderService.cs — создание заказа:
Код ITЗагрузка примера кода…
OrderEventHub подключим в шаге 7.
Minimal API
Program.cs (фрагмент):
Код ITЗагрузка примера кода…
Запуск вместе с каталогом
Терминал 1:
cd OrderDesk\catalog-api
uvicorn app.main:app --port 8100
Терминал 2:
cd OrderDesk\orders-api
dotnet run
Получите JWT и создайте заказ — подробный сценарий в Postman. Безопасность вынесена в шаг 6.
Отмена заказа и компенсация
OrderService.CancelAsync для каждой строки с ReservationId вызывает CatalogClient.DeleteReservationAsync, затем ставит статус cancelled и шлёт WebSocket-событие (см. шаг 7):
public async Task<Order?> CancelAsync(string orderId, string userId, CancellationToken ct)
{
var order = await _db.Orders.Include(o => o.Lines).FirstOrDefaultAsync(o => o.Id == orderId, ct);
if (order is null || order.UserId != userId) return null;
foreach (var line in order.Lines.Where(l => l.ReservationId is not null))
await _catalog.DeleteReservationAsync(line.ReservationId!, ct);
order.Status = OrderStatus.Cancelled;
await _db.SaveChangesAsync(ct);
await _events.PublishAsync(new OrderStatusEvent(order.Id, "cancelled"));
return order;
}
Это пример саги с компенсацией в миниатюре: прямой оркестрации без брокера достаточно для учебного стенда.
Health-check для оркестратора
app.MapGet("/health/live", () => Results.Ok(new { status = "live" }));
app.MapGet("/health/ready", async (CatalogClient catalog, CancellationToken ct) =>
{
try
{
await catalog.PingAsync(ct);
return Results.Ok(new { status = "ready", catalog = "up" });
}
catch
{
return Results.Json(new { status = "degraded", catalog = "down" }, statusCode: 503);
}
});
В CatalogClient добавьте GET /api/v1/products?pageSize=1 как лёгкий ping.
Чек-лист orders-api
-
POST /ordersпри остановленном каталоге →502 -
POST /cancelвозвращает остаток через catalogDELETE - JWT обязателен на
/api/v1/orders - В логах виден тот же
X-Request-Id, что прислал клиент