Перейти к основному содержимому

Практикум — сервис заказов на 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 возвращает остаток через catalog DELETE
  • JWT обязателен на /api/v1/orders
  • В логах виден тот же X-Request-Id, что прислал клиент