Merge pull request 'lab3' (#430) from Konovalov_arsenij_lab_3 into main

Reviewed-on: #430
This commit was merged in pull request #430.
This commit is contained in:
2025-12-08 23:11:12 +04:00
11 changed files with 437 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
# Лабораторная работа №3 - REST API, Gateway и синхронный обмен между микросервисами
## Цель работы
Изучение шаблона проектирования Gateway, построения синхронного обмена между микросервисами и архитектурного стиля RESTful API.
## Задачи
- Создать 2 микросервиса, реализующих CRUD на связанных сущностях
- Реализовать механизм синхронного обмена сообщениями между микросервисами
- Реализовать шлюз на основе прозрачного прокси-сервера nginx
## Архитектура системы
### Микросервис 1: Subscription Service
**Управление абонементами библиотеки**
- Реализует CRUD операции для сущности "Абонемент"
- Хранит данные: UUID, номер, ФИО читателя, дата выдачи
- Порт: 8081
### Микросервис 2: Book Service
**Управление книгами библиотеки**
- Реализует CRUD операции для сущности "Книга"
- Хранит данные: UUID, автор, название, год издания, UUID абонемента
- Синхронно получает информацию об абонементе через HTTP запросы
- Порт: 8082
### Gateway Service
**Единая точка входа на основе nginx**
- Маршрутизация запросов к соответствующим микросервисам
- Порт: 8080
## Связь между сущностями
Сущности связаны отношением "1-ко-многим": один абонемент может быть связан с несколькими книгами.
## Запуск проекта
### 1. Переход в директорию проекта
cd konovalov_arsenij_lab_3
### 2. Запуск приложения
docker-compose up --build
## Демонстрация работы
https://vkvideo.ru/video-234156741_456239019
Используемые технологии
ASP.NET Core 8.0 - фреймворк для разработки микросервисов
Docker - контейнеризация приложений
Docker Compose - оркестрация многоконтейнерного приложения
nginx - API Gateway и прокси-сервер
REST API - архитектурный стиль для взаимодействия микросервисов
Minimal APIs - подход к созданию веб-API в ASP.NET Core

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "BookService.dll"]

View File

@@ -0,0 +1,44 @@
namespace BookService.Models;
public record SubscriptionInfo
{
public int Number { get; init; }
public string FullName { get; init; } = string.Empty;
public DateTime Issued { get; init; }
}
public record Book
{
public Guid Uuid { get; init; }
public string Author { get; init; } = string.Empty;
public string Subject { get; init; } = string.Empty;
public int Year { get; init; }
public Guid? SubscriptionUuid { get; init; }
public SubscriptionInfo? SubscriptionInfo { get; init; }
}
public record BookList
{
public Guid Uuid { get; init; }
public string Author { get; init; } = string.Empty;
public string Subject { get; init; } = string.Empty;
public int Year { get; init; }
public Guid? SubscriptionUuid { get; init; }
}
public record CreateBookRequest
{
public string Author { get; init; } = string.Empty;
public string Subject { get; init; } = string.Empty;
public int Year { get; init; }
public Guid? SubscriptionUuid { get; init; }
}
public record UpdateBookRequest
{
public string Author { get; init; } = string.Empty;
public string Subject { get; init; } = string.Empty;
public int Year { get; init; }
public Guid? SubscriptionUuid { get; init; }
}

View File

@@ -0,0 +1,118 @@
using BookService.Models;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
var books = new Dictionary<Guid, Book>();
var subscriptionServiceUrl = Environment.GetEnvironmentVariable("SubscriptionServiceUrl") ?? "http://subscription-service";
async Task<SubscriptionInfo?> GetSubscriptionInfoAsync(Guid? subscriptionUuid, HttpClient httpClient)
{
if (subscriptionUuid == null) return null;
try
{
var response = await httpClient.GetAsync($"{subscriptionServiceUrl}/{subscriptionUuid}");
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var subscription = JsonSerializer.Deserialize<SubscriptionInfo>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return subscription;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error fetching subscription info: {ex.Message}");
}
return null;
}
app.MapGet("/", () =>
{
var bookList = books.Values.Select(b => new BookList
{
Uuid = b.Uuid,
Author = b.Author,
Subject = b.Subject,
Year = b.Year,
SubscriptionUuid = b.SubscriptionUuid
});
return Results.Ok(bookList);
});
app.MapGet("/{uuid:guid}", async (Guid uuid, HttpClient httpClient) =>
{
if (books.TryGetValue(uuid, out var book))
{
var subscriptionInfo = await GetSubscriptionInfoAsync(book.SubscriptionUuid, httpClient);
var detailedBook = book with { SubscriptionInfo = subscriptionInfo };
return Results.Ok(detailedBook);
}
return Results.NotFound();
});
app.MapPost("/", (CreateBookRequest request) =>
{
var book = new Book
{
Uuid = Guid.NewGuid(),
Author = request.Author,
Subject = request.Subject,
Year = request.Year,
SubscriptionUuid = request.SubscriptionUuid
};
books[book.Uuid] = book;
return Results.Ok(book);
});
app.MapPut("/{uuid:guid}", (Guid uuid, UpdateBookRequest request) =>
{
if (!books.ContainsKey(uuid))
{
return Results.NotFound();
}
var book = new Book
{
Uuid = uuid,
Author = request.Author,
Subject = request.Subject,
Year = request.Year,
SubscriptionUuid = request.SubscriptionUuid
};
books[uuid] = book;
return Results.Ok(book);
});
app.MapDelete("/{uuid:guid}", (Guid uuid) =>
{
if (books.Remove(uuid))
{
return Results.Ok();
}
return Results.NotFound();
});
app.Run();

View File

@@ -0,0 +1,30 @@
services:
gateway:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- .\nginx\nginx.conf:/etc/nginx/nginx.conf
depends_on:
- subscription-service
- book-service
subscription-service:
build:
context: ./subscription-service
dockerfile: Dockerfile
ports:
- "8081:80"
environment:
- ASPNETCORE_URLS=http://+:80
book-service:
build:
context: ./book-service
dockerfile: Dockerfile
ports:
- "8082:80"
environment:
- ASPNETCORE_URLS=http://+:80
- SubscriptionServiceUrl=http://subscription-service

View File

@@ -0,0 +1,36 @@
events { }
http {
upstream subscription_service {
server subscription-service:80;
}
upstream book_service {
server book-service:80;
}
server {
listen 80;
location /subscriptions/ {
proxy_pass http://subscription_service/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /books/ {
proxy_pass http://book_service/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
return 200 'Gateway is working! Use /subscriptions/ or /books/';
add_header Content-Type text/plain;
}
}
}

View File

@@ -0,0 +1,11 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "SubscriptionService.dll"]

View File

@@ -0,0 +1,23 @@
namespace SubscriptionService.Models;
public record Subscription
{
public Guid Uuid { get; init; }
public int Number { get; init; }
public string FullName { get; init; } = string.Empty;
public DateTime Issued { get; init; }
}
public record CreateSubscriptionRequest
{
public int Number { get; init; }
public string FullName { get; init; } = string.Empty;
public DateTime Issued { get; init; }
}
public record UpdateSubscriptionRequest
{
public int Number { get; init; }
public string FullName { get; init; } = string.Empty;
public DateTime Issued { get; init; }
}

View File

@@ -0,0 +1,72 @@
using SubscriptionService.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var subscriptions = new Dictionary<Guid, Subscription>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGet("/", () => Results.Ok(subscriptions.Values));
app.MapGet("/{uuid:guid}", (Guid uuid) =>
{
if (subscriptions.TryGetValue(uuid, out var subscription))
{
return Results.Ok(subscription);
}
return Results.NotFound();
});
app.MapPost("/", (CreateSubscriptionRequest request) =>
{
var subscription = new Subscription
{
Uuid = Guid.NewGuid(),
Number = request.Number,
FullName = request.FullName,
Issued = request.Issued
};
subscriptions[subscription.Uuid] = subscription;
return Results.Ok(subscription);
});
app.MapPut("/{uuid:guid}", (Guid uuid, UpdateSubscriptionRequest request) =>
{
if (!subscriptions.ContainsKey(uuid))
{
return Results.NotFound();
}
var subscription = new Subscription
{
Uuid = uuid,
Number = request.Number,
FullName = request.FullName,
Issued = request.Issued
};
subscriptions[uuid] = subscription;
return Results.Ok(subscription);
});
app.MapDelete("/{uuid:guid}", (Guid uuid) =>
{
if (subscriptions.Remove(uuid))
{
return Results.Ok();
}
return Results.NotFound();
});
app.Run("http://*:80");

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
</Project>