Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
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>№1 Кеширование </Strong></UnorderedListItem>
<UnorderedListItem>Вариант <Strong>№45 Товар на складе</Strong></UnorderedListItem>
<UnorderedListItem>Выполнена <Strong> Ле Хань Хоанг 6513</Strong> </UnorderedListItem>
<UnorderedListItem><Link To="https://github.com/vieKH/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": ""
"BaseAddress": "https://localhost:7266/api/inventory"
}
22 changes: 20 additions & 2 deletions CloudDevelopment.sln
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36811.4
# Visual Studio Version 18
VisualStudioVersion = 18.3.11520.95 d18.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inventory.ApiService", "InventoryManager\Inventory.ApiService\Inventory.ApiService.csproj", "{E32F4311-CD79-A2FA-0DBA-55DAD48F6377}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inventory.AppHost", "InventoryManager\Inventory.AppHost\Inventory.AppHost.csproj", "{AEBB0C4B-1B1C-46E8-ED62-4289C5A76CEE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inventory.ServiceDefaults", "InventoryManager\Inventory.ServiceDefaults\Inventory.ServiceDefaults.csproj", "{E302BFA1-84FC-63A7-EA3A-7872A83042B9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +21,18 @@ Global
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU
{E32F4311-CD79-A2FA-0DBA-55DAD48F6377}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E32F4311-CD79-A2FA-0DBA-55DAD48F6377}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E32F4311-CD79-A2FA-0DBA-55DAD48F6377}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E32F4311-CD79-A2FA-0DBA-55DAD48F6377}.Release|Any CPU.Build.0 = Release|Any CPU
{AEBB0C4B-1B1C-46E8-ED62-4289C5A76CEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AEBB0C4B-1B1C-46E8-ED62-4289C5A76CEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AEBB0C4B-1B1C-46E8-ED62-4289C5A76CEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AEBB0C4B-1B1C-46E8-ED62-4289C5A76CEE}.Release|Any CPU.Build.0 = Release|Any CPU
{E302BFA1-84FC-63A7-EA3A-7872A83042B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E302BFA1-84FC-63A7-EA3A-7872A83042B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E302BFA1-84FC-63A7-EA3A-7872A83042B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E302BFA1-84FC-63A7-EA3A-7872A83042B9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
17 changes: 17 additions & 0 deletions InventoryManager/Inventory.ApiService/Cache/IInventoryCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Inventory.ApiService.Entity;

namespace Inventory.ApiService.Cache;

/// <summary>
/// Интерфейс сервиса для получения продукта с использованием кэширования.
/// </summary>
public interface IInventoryCache
{
/// <summary>
/// Возвращает продукт по идентификатору из кэша или генерирует его при отсутствии в кэше.
/// </summary>
/// <param name="id"> Идентификатор продукта</param>
/// <param name="ct"> Токен отмены операции</param>
/// <returns> Экземпляр продукта</returns>
public Task<Product> GetAsync(int id, CancellationToken ct);
}
84 changes: 84 additions & 0 deletions InventoryManager/Inventory.ApiService/Cache/InventoryCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Text.Json;
using Inventory.ApiService.Entity;
using Inventory.ApiService.Generation;
using Microsoft.Extensions.Caching.Distributed;

namespace Inventory.ApiService.Cache;
/// <summary>
/// Реализация сервиса кэширования для получения продукта.
/// Сначала пытается получить данные из кэша, при отсутствии — генерирует продукт и сохраняет его в кэш.
/// </summary>
/// <param name="cache"> Сервис распределённого кэширования</param>
/// <param name="configuration"> Конфигурация приложения</param>
/// <param name="logger"> Логгер для записи событий</param>
/// <param name="generator"> Генератор </param>
public class InventoryCache(IDistributedCache cache, IConfiguration configuration, ILogger<InventoryCache> logger,Generator generator) : IInventoryCache
{
/// <summary>
/// Возвращает продукт по идентификатору.
/// При наличии в кэше возвращает сохранённые данные, иначе генерирует новый объект и сохраняет его в кэш
/// </summary>
/// <param name="id"> Идентификатор продукта</param>
/// <param name="ct"> Токен отмены операции</param>
/// <returns></returns>
public async Task<Product> GetAsync(int id, CancellationToken ct)
{
var cacheKey = $"inventory-{id}";
logger.LogInformation("Try get product {Id} from cache", id);

string? cachedData = null;

try
{
cachedData = await cache.GetStringAsync(cacheKey, ct);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Cache READ failed for {Id}. Continue without cache.", id);
}

if (!string.IsNullOrEmpty(cachedData))
{
try
{
var cachedProduct = JsonSerializer.Deserialize<Product>(cachedData);
if (cachedProduct is not null)
{
logger.LogInformation("Cache HIT for product {Id}", id);
return cachedProduct;
}

logger.LogWarning("Cache HIT but deserialize returned null for product {Id}", id);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Deserialize failed for product {Id}. Continue without cache.", id);
}
}

logger.LogInformation("Cache MISS for product {Id}. Generating.", id);
var product = generator.Generate(id);

try
{
var expirationMinutes = configuration.GetValue("CacheSettings:ExpirationMinutes", 5);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(expirationMinutes)
};

await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(product), options, ct);
logger.LogInformation("Product {Id} saved to cache", id);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Cache WRITE failed for {Id}. Continue without cache.", id);
}

return product;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Mvc;
using Inventory.ApiService.Entity;
using Inventory.ApiService.Cache;
using Inventory.ApiService.Generation;

namespace Inventory.ApiService.Controllers;

/// <summary>
/// Контроллер для обработки запросов, связанных с продуктами
/// </summary>
/// <param name="cache"> Сервис кэширования продуктов</param>
[ApiController]
[Route("api/[controller]")]
public class InventoryController(IInventoryCache cache) : ControllerBase
{
/// <summary>
/// Обрабатывает GET-запрос на получение продукта по идентификатору
/// </summary>
/// <param name="id"> Идентификатор продукта</param>
/// <param name="ct"> Токен отмены операции</param>
/// <returns> Объект продукта или ошибка 400 при некорректном идентификаторе</returns>
[HttpGet]
[ProducesResponseType(typeof(Product), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Product>> Get([FromQuery] int? id, CancellationToken ct)
{
if (id is null || id < 0)
return BadRequest("id is required and must be >= 0");

var product = await cache.GetAsync(id.Value, ct);
return Ok(product);
}
}
57 changes: 57 additions & 0 deletions InventoryManager/Inventory.ApiService/Entity/Product.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace Inventory.ApiService.Entity;

/// <summary>
/// Класс, представляющий товар на складе
/// </summary>
public class Product
{
/// <summary>
/// Идентификатор в системе
/// </summary>
public int Id { get; set; }

/// <summary>
/// Наименование товара
/// </summary>
public string NameProduct { get; set; } = string.Empty;

/// <summary>
/// Категория товара
/// </summary>
public string Category { get; set; } = string.Empty;

/// <summary>
/// Количество на складе
/// </summary>
public int Quantity { get; set; }

/// <summary>
/// Цена за единицу товара
/// </summary>
public decimal Price { get; set; }

/// <summary>
/// Вес единицы товара
/// </summary>
public double Weight { get; set; }

/// <summary>
/// Габариты единицы товара
/// </summary>
public string Dimension { get; set; } = string.Empty;

/// <summary>
/// Товар хрупкий
/// </summary>
public bool IsFragile { get; set; }

/// <summary>
/// Дата последней поставки
/// </summary>
public DateOnly LastDeliveryDate { get; set; }

/// <summary>
/// Дата следующей поставки
/// </summary>
public DateOnly NextDeliveryDate { get; set; }
}
43 changes: 43 additions & 0 deletions InventoryManager/Inventory.ApiService/Generation/Generator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Bogus;
using Inventory.ApiService.Entity;

namespace Inventory.ApiService.Generation;
/// <summary>
/// Сервис генерации тестовых данных продукта.Использует библиотеку Bogus для создания случайных значений.
/// </summary>
public class Generator
{
private static readonly Faker<Product> _faker = new Faker<Product>()
.RuleFor(x => x.NameProduct, f => f.Commerce.ProductName())
.RuleFor(x => x.Category, f => f.Commerce.Categories(1)[0])
.RuleFor(x => x.Quantity, f => f.Random.Int(0, 1000))
.RuleFor(x => x.Price, f => Math.Round(f.Random.Decimal(1, 10000), 2))
.RuleFor(x => x.Weight, f => Math.Round(f.Random.Double(0.1, 100), 2))
.RuleFor(x => x.Dimension, f =>
{
var a = f.Random.Int(1, 200);
var b = f.Random.Int(1, 200);
var c = f.Random.Int(1, 200);
return $"{a}×{b}×{c} cm";
})
.RuleFor(x => x.IsFragile, f => f.Random.Bool())
.RuleFor(x => x.LastDeliveryDate, f => DateOnly.FromDateTime(f.Date.Past(2)))
.RuleFor(x => x.NextDeliveryDate, (f, item) =>
{
var lastDate = item.LastDeliveryDate.ToDateTime(TimeOnly.MinValue);
var nextDate = f.Date.Between(lastDate, lastDate.AddMonths(6));
return DateOnly.FromDateTime(nextDate);
});

/// <summary>
/// Генерирует продукт по заданному идентификатору.
/// </summary>
/// <param name="id"> Идентификатор продукта</param>
/// <returns> Сгенерированный объект продукта</returns>
public Product Generate(int id)
{
var product = _faker.Generate();
product.Id = id;
return product;
}
}
20 changes: 20 additions & 0 deletions InventoryManager/Inventory.ApiService/Inventory.ApiService.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

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

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

<ItemGroup>
<PackageReference Include="Aspire.StackExchange.Redis.DistributedCaching" Version="13.1.1" />
<PackageReference Include="Bogus" Version="35.6.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.4" />
</ItemGroup>

</Project>
41 changes: 41 additions & 0 deletions InventoryManager/Inventory.ApiService/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Inventory.ApiService.Cache;
using Inventory.ApiService.Generation;
using Inventory.ServiceDefaults;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

// Cache
builder.AddRedisDistributedCache("cache");

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

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

// DI
builder.Services.AddSingleton<Generator>();
builder.Services.AddScoped<IInventoryCache, InventoryCache>();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

app.UseCors("client");

app.MapControllers();
app.MapDefaultEndpoints();

app.Run();
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:5339",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7266;http://localhost:5339",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Loading