Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
28984db
Добавлена генерация контрактов с использованием Bogus, подключен Redi…
Tomashaytis Feb 21, 2026
4a4d2fb
Добавлен контроллер и кеширование с использованием Redis.
Tomashaytis Feb 21, 2026
6bd53d5
Добавлен WEB-интерфейс для redis, summaries и защита от недоступности…
Tomashaytis Feb 22, 2026
0f29d84
Добавлены сервис для сущности Курс и сервис для взаимодействия с кэшем
Tomashaytis Feb 22, 2026
f0bfce4
Скорректированы поля сущности Курс согласно варианту.
Tomashaytis Feb 22, 2026
9cd62ce
Update README.md
Tomashaytis Feb 22, 2026
451c4f9
Подправлено структурное логирование
Tomashaytis Feb 22, 2026
0fa0334
Исправил замечания
Tomashaytis Feb 24, 2026
55a48f7
Добавлен Api Gateway Ocelot, добавлены 3 реплики сервиса генерации да…
Tomashaytis Mar 6, 2026
6d642cf
Добавлен Query Based балансировщик нагрузки
Tomashaytis Mar 7, 2026
ffaa9c9
Добавлено создание реплик в цикле, summary к балансировщику нагрузки
Tomashaytis Mar 7, 2026
2393615
Исправлена ошибка, из-за которой ApiGateway не использовался; добавле…
Tomashaytis Mar 7, 2026
634ca0c
Update README.md
Tomashaytis Mar 7, 2026
7b673a5
Изменена структура проекта
Tomashaytis Mar 7, 2026
a343787
Update README.md
Tomashaytis Mar 10, 2026
44b56cc
Исправлены замечания
Tomashaytis Mar 10, 2026
1195e4e
Merge branch 'main' of https://github.com/Tomashaytis/cloud-development
Tomashaytis Mar 10, 2026
00a398b
Небольшие улучшения
Tomashaytis Mar 11, 2026
34904c7
Создана конфигурация для SNS и S3
Tomashaytis Apr 11, 2026
39e849a
Добавлен проект для подписки на события брокера сообщений SNS и возмо…
Tomashaytis Apr 12, 2026
f3c002d
Внесены значительные изменения в проект, чтобы его удалось запустить …
Tomashaytis Apr 14, 2026
0eff8c2
Реализованы интеграционные тесты
Tomashaytis Apr 14, 2026
85e7e38
Частичное упрощение кода
Tomashaytis Apr 14, 2026
fc0caca
Изменение номера ЛР.
Tomashaytis Apr 15, 2026
a4fd9d3
Update README.md
Tomashaytis Apr 15, 2026
6fa883b
Небольшие улучшения кода
Tomashaytis Apr 15, 2026
069217e
Merge branch 'main' of https://github.com/Tomashaytis/cloud-development
Tomashaytis Apr 15, 2026
f6c1fa0
Поправил SnsPublisherService
Tomashaytis Apr 17, 2026
acc2e86
Изменил структуру приложения под использование CloudFormation.
Tomashaytis Apr 18, 2026
08b292b
Перешёл на slnx.
Tomashaytis Apr 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Client.Wasm/Client.Wasm.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
<None Remove="Components\StudentCard.razor~RF1bb17a4.TMP" />
</ItemGroup>

<ItemGroup>
<None Include="Client.Wasm.csproj.user" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Blazorise" Version="1.8.8" />
<PackageReference Include="Blazorise.Bootstrap" Version="1.8.8" />
Expand Down
8 changes: 4 additions & 4 deletions Client.Wasm/Components/StudentCard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
</CardHeader>
<CardBody>
<UnorderedList Unstyled>
<UnorderedListItem>Номер <Strong>№X "Название лабораторной"</Strong></UnorderedListItem>
<UnorderedListItem>Вариант <Strong>№Х "Название варианта"</Strong></UnorderedListItem>
<UnorderedListItem>Выполнена <Strong>Фамилией Именем 65ХХ</Strong> </UnorderedListItem>
<UnorderedListItem><Link To="https://puginarug.com/">Ссылка на форк</Link></UnorderedListItem>
<UnorderedListItem>Номер <Strong>№ 3</Strong></UnorderedListItem>
<UnorderedListItem>Вариант <Strong>№ 52</Strong></UnorderedListItem>
<UnorderedListItem>Выполнена <Strong>Томашайтисом Павлом 6513</Strong> </UnorderedListItem>
<UnorderedListItem><Link To="https://github.com/Tomashaytis/cloud-development">Ссылка на форк</Link></UnorderedListItem>
</UnorderedList>
</CardBody>
</Card>
2 changes: 1 addition & 1 deletion Client.Wasm/wwwroot/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
}
},
"AllowedHosts": "*",
"BaseAddress": "https://localhost:7170/land-plot"
"BaseAddress": "https://localhost:5000/course-management"
}
25 changes: 0 additions & 25 deletions CloudDevelopment.sln

This file was deleted.

18 changes: 18 additions & 0 deletions CourseManagement.ApiGateway/CourseManagement.ApiGateway.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
<PackageReference Include="Ocelot" Version="24.1.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CourseManagement.ServiceDefaults\CourseManagement.ServiceDefaults.csproj" />
</ItemGroup>

</Project>
44 changes: 44 additions & 0 deletions CourseManagement.ApiGateway/LoadBalancers/QueryBased.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Ocelot.LoadBalancer.Interfaces;
using Ocelot.Responses;
using Ocelot.Values;

namespace CourseManagement.ApiGateway.LoadBalancers;

/// <summary>
/// Балансировщик нагрузки на основе запроса
/// </summary>
/// <param name="logger">Логгер</param>
/// <param name="services">Список сервисов</param>
public class QueryBased(ILogger<QueryBased> logger, List<Service> services) : ILoadBalancer
{
/// <summary>
/// Тип балансировщика нагрузки
/// </summary>
public string Type => nameof(QueryBased);

/// <summary>
/// Метод выбора сервиса на основе запроса
/// </summary>
/// <param name="httpContext">HTTP запрос</param>
/// <returns>Выбранный сервис</returns>
public Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext)
{
var idString = httpContext.Request.Query["id"].FirstOrDefault();

if (!int.TryParse(idString, out var id))
id = 0;

var service = services[id % services.Count];

logger.LogInformation("Request {ResourceId} sent to service on port {servicePort}", id, service.HostAndPort.DownstreamPort);

return Task.FromResult<Response<ServiceHostAndPort>>(
new OkResponse<ServiceHostAndPort>(service.HostAndPort));
}

/// <summary>
/// Метод очистки ресурсов для сервиса
/// </summary>
/// <param name="hostAndPort">Параметры сервиса</param>
public void Release(ServiceHostAndPort hostAndPort) { }
}
40 changes: 40 additions & 0 deletions CourseManagement.ApiGateway/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using CourseManagement.ApiGateway.LoadBalancers;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

// Подключение конфига Ocelot
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);

// Добавление Ocelot с Query Based балансировщиком нагрузки
builder.Services.AddOcelot()
.AddCustomLoadBalancer<QueryBased>((serviceProvider, downstreamRoute, serviceDiscoveryProvider) =>
{
var logger = serviceProvider.GetRequiredService<ILogger<QueryBased>>();

var services = serviceDiscoveryProvider.GetAsync().GetAwaiter().GetResult().ToList();

return new QueryBased(logger, services);
});

// CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowClient", policy =>
{
policy.AllowAnyOrigin()
.WithMethods("GET")
.WithHeaders("Content-Type");
});
});

var app = builder.Build();

app.UseCors("AllowClient");

await app.UseOcelot();

app.Run();
23 changes: 23 additions & 0 deletions CourseManagement.ApiGateway/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5256",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7253;http://localhost:5256",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
8 changes: 8 additions & 0 deletions CourseManagement.ApiGateway/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions CourseManagement.ApiGateway/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
72 changes: 72 additions & 0 deletions CourseManagement.ApiGateway/ocelot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"Routes": [
{
"DownstreamPathTemplate": "/course-management",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 8081
},
{
"Host": "localhost",
"Port": 8082
},
{
"Host": "localhost",
"Port": 8083
},
{
"Host": "localhost",
"Port": 8084
},
{
"Host": "localhost",
"Port": 8085
}
],
"UpstreamPathTemplate": "/course-management",
"UpstreamHttpMethod": [ "GET" ],
"LoadBalancerOptions": {
"Type": "QueryBased",
"Key": "course-api"
}
},
{
"DownstreamPathTemplate": "/health",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 8081
},
{
"Host": "localhost",
"Port": 8082
},
{
"Host": "localhost",
"Port": 8083
},
{
"Host": "localhost",
"Port": 8084
},
{
"Host": "localhost",
"Port": 8085
}
],
"UpstreamPathTemplate": "/health",
"UpstreamHttpMethod": [ "GET" ],
"Priority": 1,
"LoadBalancerOptions": {
"Type": "QueryBased",
"Key": "course-api-health"
}
}
],
"GlobalConfiguration": {
"BaseUrl": "https://localhost:5000"
}
}
66 changes: 66 additions & 0 deletions CourseManagement.ApiService/Cache/CacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;

namespace CourseManagement.ApiService.Cache;

/// <summary>
/// Универсальный сервис для взаимодействия с кэшем
/// </summary>
/// <param name="logger">Логгер</param>
/// <param name="cache">Кэш</param>
public class CacheService<T>(ILogger<CacheService<T>> logger, IDistributedCache cache) : ICacheService<T>
{
/// <summary>
/// Время жизни данных в кэше
/// </summary>
private static readonly double _cacheDuration = 5;

/// <inheritdoc/>
public async Task<T?> FetchAsync(string key, int id)
{
var cacheKey = $"{key}:{id}";

try
{
var cached = await cache.GetStringAsync(cacheKey);
if (cached != null)
{
var obj = JsonSerializer.Deserialize<T>(cached);

logger.LogInformation("Cache hit for {EntityType} {ResourceId}", typeof(T).Name, id);

return obj;
}

logger.LogInformation("Cache miss for {EntityType} {ResourceId}", typeof(T).Name, id);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Сache is unavailable");
}

return default;
}

/// <inheritdoc/>
public async Task StoreAsync(string key, int id, T entity)
{
var cacheKey = $"{key}:{id}";

var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_cacheDuration)
};

try
{
var serialized = JsonSerializer.Serialize(entity);
await cache.SetStringAsync(cacheKey, serialized, options);
logger.LogInformation("{EntityType} {ResourceId} cached", typeof(T).Name, id);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Сache is unavailable");
}
}
}
23 changes: 23 additions & 0 deletions CourseManagement.ApiService/Cache/ICacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace CourseManagement.ApiService.Cache;

/// <summary>
/// Универсальный интерфейс для работы с кэшем
/// </summary>
public interface ICacheService<T>
{
/// <summary>
/// Асинхронный метод для извлечения данных из кэша
/// </summary>
/// <param name="key">Ключ для сущности</param>
/// <param name="id">Идентификатор сущности</param>
/// <returns>Объект или null при его отсутствии</returns>
public Task<T?> FetchAsync(string key, int id);

/// <summary>
/// Асинхронный метод для внесения данных в кэш
/// </summary>
/// <param name="key">Ключ для сущности</param>
/// <param name="id">Идентификатор сущности</param>
/// <param name="entity">Кешируемая сущность</param>
public Task StoreAsync(string key, int id, T entity);
}
Loading