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:
62
Konovalov_arsenij_lab_3/Readme.md
Normal file
62
Konovalov_arsenij_lab_3/Readme.md
Normal 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
|
||||
|
||||
|
||||
15
Konovalov_arsenij_lab_3/book-service/BookService.csproj
Normal file
15
Konovalov_arsenij_lab_3/book-service/BookService.csproj
Normal 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>
|
||||
11
Konovalov_arsenij_lab_3/book-service/Dockerfile
Normal file
11
Konovalov_arsenij_lab_3/book-service/Dockerfile
Normal 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"]
|
||||
44
Konovalov_arsenij_lab_3/book-service/Models/Book.cs
Normal file
44
Konovalov_arsenij_lab_3/book-service/Models/Book.cs
Normal 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; }
|
||||
}
|
||||
118
Konovalov_arsenij_lab_3/book-service/Program.cs
Normal file
118
Konovalov_arsenij_lab_3/book-service/Program.cs
Normal 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();
|
||||
30
Konovalov_arsenij_lab_3/docker-compose.yml
Normal file
30
Konovalov_arsenij_lab_3/docker-compose.yml
Normal 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
|
||||
36
Konovalov_arsenij_lab_3/nginx/nginx.conf
Normal file
36
Konovalov_arsenij_lab_3/nginx/nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Konovalov_arsenij_lab_3/subscription-service/Dockerfile
Normal file
11
Konovalov_arsenij_lab_3/subscription-service/Dockerfile
Normal 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"]
|
||||
@@ -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; }
|
||||
}
|
||||
72
Konovalov_arsenij_lab_3/subscription-service/Program.cs
Normal file
72
Konovalov_arsenij_lab_3/subscription-service/Program.cs
Normal 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");
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user