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
Binary file added .DS_Store
Binary file not shown.
34 changes: 34 additions & 0 deletions AspireApp.ApiService/AspireApp.ApiService.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

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

<PropertyGroup Condition=" '$(RunConfiguration)' == 'http' " />
<ItemGroup>
<ProjectReference Include="..\AspireApp.ServiceDefaults\AspireApp.ServiceDefaults.csproj">
<ReferenceSourceTarget></ReferenceSourceTarget>
<GlobalPropertiesToRemove></GlobalPropertiesToRemove>
</ProjectReference>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.5" />
<PackageReference Include="Aspire.StackExchange.Redis.DistributedCaching" Version="9.5.0" />
</ItemGroup>

<ItemGroup>
<None Remove="Properties\Entities\" />
<None Remove="Properties\Generator\" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\Entities\" />
<Folder Include="Properties\Generator\" />
</ItemGroup>
</Project>
6 changes: 6 additions & 0 deletions AspireApp.ApiService/AspireApp.ApiService.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@ApiService_HostAddress = http://localhost:5391

GET {{ApiService_HostAddress}}/weatherforecast/
Accept: application/json

###
38 changes: 38 additions & 0 deletions AspireApp.ApiService/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using AspireApp.ApiService.Properties.Generator;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.AddRedisDistributedCache("RedisCache");

builder.Services.AddScoped<IWarehouseCache, WarehouseCache>();
builder.Services.AddScoped<WarehouseGenerator>();
builder.Services.AddScoped<IWarehouseGeneratorService, WarehouseGeneratorService>();

builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});

var app = builder.Build();

app.MapDefaultEndpoints();

app.MapGet("/warehouse", async (IWarehouseGeneratorService service, int id) =>
{
var warehouse = await service.ProcessWarehouse(id);
return Results.Ok(warehouse);
});

app.UseCors();

app.Run();
70 changes: 70 additions & 0 deletions AspireApp.ApiService/Properties/Entities/Warehouse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
namespace AspireApp.ApiService.Properties.Entities;
using System.Text.Json.Serialization;


/// <summary>
/// Товар на складе
/// </summary>
public class Warehouse
{
/// <summary>
/// Идентификатор
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }

/// <summary>
/// Наименование товара
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; set; }

/// <summary>
/// Категория товара
/// </summary>
[JsonPropertyName("category")]
public string? Category { get; set; }

/// <summary>
/// Количество на складе
/// </summary>
[JsonPropertyName("stockQuantity")]
public int StockQuantity { get; set; }

/// <summary>
/// Цена за единицу товара
/// </summary>
[JsonPropertyName("price")]
public decimal Price { get; set; }

/// <summary>
/// Вес единицы товара
/// </summary>
[JsonPropertyName("weight")]
public double Weight { get; set; }

/// <summary>
/// Габариты единицы товара (формат: ДхШхВ см)
/// </summary>
[JsonPropertyName("dimensions")]
public string? Dimensions { get; set; }

/// <summary>
/// Хрупкий ли товар
/// </summary>
[JsonPropertyName("isFragile")]
public bool IsFragile { get; set; }

/// <summary>
/// Дата последней поставки
/// </summary>
[JsonPropertyName("lastDeliveryDate")]
public DateOnly LastDeliveryDate { get; set; }

/// <summary>
/// Дата следующей планируемой поставки
/// </summary>
[JsonPropertyName("nextDeliveryDate")]
public DateOnly NextDeliveryDate { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using AspireApp.ApiService.Properties.Entities;
namespace AspireApp.ApiService.Properties.Generator;

/// <summary>
/// Интерфейс для запуска юзкейса
/// </summary>
public interface IWarehouseGeneratorService
{
/// <summary>
/// Обработка запроса на генерацию товара
/// </summary>
public Task<Warehouse> ProcessWarehouse(int id);
}
62 changes: 62 additions & 0 deletions AspireApp.ApiService/Properties/Generator/WarehouseCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using AspireApp.ApiService.Properties.Entities;

namespace AspireApp.ApiService.Properties.Generator;

/// <summary>
/// Сервис для работы с кэшем товаров на складе
/// </summary>
public interface IWarehouseCache
{
/// <summary>Получить товар из кэша по идентификатору</summary>
Task<Warehouse?> GetAsync(int id);

/// <summary>Сохранить товар в кэш с временем</summary>
Task SetAsync(Warehouse warehouse, TimeSpan expiration);
}

/// <summary>
/// Кэширование товаров
/// </summary>
public class WarehouseCache : IWarehouseCache
{
private readonly IDistributedCache _cache;
private readonly ILogger<WarehouseCache> _logger;

public WarehouseCache(IDistributedCache cache, ILogger<WarehouseCache> logger)
{
_cache = cache;
_logger = logger;
}

public async Task<Warehouse?> GetAsync(int id)
{
var key = $"warehouse_{id}";
var cached = await _cache.GetStringAsync(key);
if (cached == null)
return null;

try
{
return JsonSerializer.Deserialize<Warehouse>(cached);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка десериализации товара {Id} из кэша", id);
return null;
}
}

public async Task SetAsync(Warehouse warehouse, TimeSpan expiration)
{
var key = $"warehouse_{warehouse.Id}";
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration
};
var serialized = JsonSerializer.Serialize(warehouse);
await _cache.SetStringAsync(key, serialized, options);
}
}
41 changes: 41 additions & 0 deletions AspireApp.ApiService/Properties/Generator/WarehouseGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Bogus;
using AspireApp.ApiService.Properties.Entities;

namespace AspireApp.ApiService.Properties.Generator;

/// <summary>
/// Генератор случайных данных для товаров на складе
/// </summary>
public class WarehouseGenerator
{
private readonly Faker<Warehouse> _faker;

public WarehouseGenerator()
{
_faker = new Faker<Warehouse>()
.RuleFor(p => p.Id, f => f.IndexGlobal + 1)
.RuleFor(p => p.Name, f => f.Commerce.ProductName())
.RuleFor(p => p.Category, f => f.Commerce.Categories(1)[0])
.RuleFor(p => p.StockQuantity, f => f.Random.Int(0, 1000))
.RuleFor(p => p.Price, f => Math.Round(f.Random.Decimal(1, 10000), 2))
.RuleFor(p => p.Weight, f => Math.Round(f.Random.Double(0.1, 100), 2))
.RuleFor(p => p.Dimensions, f =>
$"{f.Random.Int(1, 100)}х{f.Random.Int(1, 100)}х{f.Random.Int(1, 100)} см")
.RuleFor(p => p.IsFragile, f => f.Random.Bool(0.3f))
.RuleFor(p => p.LastDeliveryDate, f => DateOnly.FromDateTime(f.Date.Past(1)))
.RuleFor(p => p.NextDeliveryDate, (f, p) =>
DateOnly.FromDateTime(
f.Date.Soon(30, p.LastDeliveryDate.ToDateTime(TimeOnly.MinValue))
));
}

/// <summary>
/// Генерация случайного товара
/// </summary>
public Warehouse Generate() => _faker.Generate();

/// <summary>
/// Генерация нескольких случайных товаров
/// </summary>
public List<Warehouse> Generate(int count) => _faker.Generate(count);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using AspireApp.ApiService.Properties.Entities;

namespace AspireApp.ApiService.Properties.Generator;

/// <summary>
/// Служба для запуска юзкейса по обработке товаров на складе
/// </summary>
public class WarehouseGeneratorService(
IWarehouseCache warehouseCache,
IConfiguration configuration,
ILogger<WarehouseGeneratorService> logger,
WarehouseGenerator generator) : IWarehouseGeneratorService
{
/// <summary>
/// Время инвалидации кэша
/// </summary>
private readonly TimeSpan _cacheExpiration = int.TryParse(configuration["CacheExpiration"], out var seconds)
? TimeSpan.FromSeconds(seconds)
: TimeSpan.FromSeconds(3600);

public async Task<Warehouse> ProcessWarehouse(int id)
{
logger.LogInformation("Обработка товара с Id = {Id} начата", id);

// Получаем товар из кэша
Warehouse? warehouse = null;
try
{
warehouse = await warehouseCache.GetAsync(id);
if (warehouse != null)
{
logger.LogInformation("Товар {Id} получен из кэша", id);
return warehouse;
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Не удалось получить товар {Id} из кэша (ошибка игнорируется)", id);
}

// Если в кэше нет или ошибка — генерируем новый товар
logger.LogInformation("Товар {Id} в кэше не найден или кэш недоступен, запуск генерации", id);
warehouse = generator.Generate();
warehouse.Id = id;

// Попытка сохранить в кэш
try
{
logger.LogInformation("Сохранение товара {Id} в кэш", id);
await warehouseCache.SetAsync(warehouse, _cacheExpiration);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Не удалось сохранить товар {Id} в кэш (ошибка игнорируется)", id);
}

return warehouse;
}
}
13 changes: 13 additions & 0 deletions AspireApp.ApiService/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
14 changes: 14 additions & 0 deletions AspireApp.ApiService/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{"BaseAddress": "http://localhost:53677",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedOrigins": [
"https://localhost:3000",
"https://localhost:62108",
"http://localhost:62109"
]
}

9 changes: 9 additions & 0 deletions AspireApp.ApiService/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
13 changes: 13 additions & 0 deletions AspireApp.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("RedisCache").WithRedisInsight(containerName: "insight");

var service = builder.AddProject<Projects.AspireApp_ApiService>("service-api")
.WithReference(cache)
.WaitFor(cache);

builder.AddProject<Projects.Client_Wasm>("client-wasm")
.WithReference(service)
.WaitFor(service);

builder.Build().Run();
Loading