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
6 changes: 3 additions & 3 deletions Client.Wasm/Client.Wasm.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand All @@ -14,8 +14,8 @@
<PackageReference Include="Blazorise" Version="1.8.8" />
<PackageReference Include="Blazorise.Bootstrap" Version="1.8.8" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.8.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.22" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.22" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.7" PrivateAssets="all" />
</ItemGroup>

</Project>
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>№48 "Сотрудник компании"</Strong></UnorderedListItem>
<UnorderedListItem>Выполнена <Strong>Ненашевым Дмитрием 6513</Strong> </UnorderedListItem>
<UnorderedListItem><Link To="https://github.com/neygenius/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:7297/employee"
}
19 changes: 19 additions & 0 deletions Cloud.API/Cloud.API.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

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

<ItemGroup>
<PackageReference Include="Aspire.StackExchange.Redis.DistributedCaching" Version="13.2.4" />
<PackageReference Include="Bogus" Version="35.6.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

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

</Project>
39 changes: 39 additions & 0 deletions Cloud.API/Controllers/EmployeeController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Cloud.API.Services;
using Cloud.API.Models;
using Microsoft.AspNetCore.Mvc;

namespace Cloud.API.Controllers;

/// <summary>
/// Контроллер для получения сотрудника компании по id
/// </summary>
/// <param name="employeeService">Сервис получения сотрудника компании</param>
/// <param name="logger">Логгер</param>
[ApiController]
[Route("employee")]
public class EmployeesController(
IEmployeeService employeeService,
ILogger<EmployeesController> logger
) : ControllerBase
{
/// <summary>
/// Метод для получения сотрудника компании по id
/// </summary>
/// <param name="id">Идентификатор сотрудника</param>
/// <returns>Информация о сотруднике компании</returns>
/// <response code="200">Успешное получение сотрудника компании</response>
/// <response code="400">Некорректный id сотрудника</response>
[ProducesResponseType(typeof(Employee), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[HttpGet]
public async Task<ActionResult<Employee>> GetEmployee([FromQuery] int id)
{
if (id <= 0)
return BadRequest(new { error = "Id must be a positive number" });

logger.LogInformation("HTTP GET /employee, id: {employeeId}", id);

var employee = await employeeService.GetOrGenerateAsync(id);
return Ok(employee);
}
}
48 changes: 48 additions & 0 deletions Cloud.API/Models/Employee.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace Cloud.API.Models;

/// <summary>
/// Информация о сотруднике компании
/// </summary>
public class Employee
{
/// <summary>
/// Идентификатор сотрудника в системе
/// </summary>
public required int Id { get; set; }
/// <summary>
/// ФИО
/// </summary>
public required string FullName { get; set; }
/// <summary>
/// Должность
/// </summary>
public required string Position { get; set; }
/// <summary>
/// Отдел
/// </summary>
public required string Department { get; set; }
/// <summary>
/// Дата приема
/// </summary>
public DateOnly HireDate { get; set; }
/// <summary>
/// Зарплата
/// </summary>
public required decimal Salary { get; set; }
/// <summary>
/// Электронная почта
/// </summary>
public required string Email { get; set; }
/// <summary>
/// Номер телефона
/// </summary>
public required string PhoneNumber { get; set; }
/// <summary>
/// Индикатор увольнения
/// </summary>
public required bool IsFired { get; set; }
/// <summary>
/// Дата увольнения
/// </summary>
public DateOnly? FiredDate { get; set; }
}
41 changes: 41 additions & 0 deletions Cloud.API/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Cloud.Api.Services;
using Cloud.API.Services;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();
builder.AddRedisDistributedCache("redis");

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

builder.Services.AddSingleton<IEmployeeGenerator, EmployeeGenerator>();
builder.Services.AddScoped<IEmployeeService, EmployeeService>();

builder.Services.AddCors(options =>
{
options.AddPolicy("LocalPolicy", policy =>
{
policy
.SetIsOriginAllowed(origin => origin.StartsWith("https://localhost"))
.WithHeaders("Content-Type")
.WithMethods("GET");
});
});

var app = builder.Build();

app.MapDefaultEndpoints();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors("LocalPolicy");
app.MapControllers();

app.Run();
41 changes: 41 additions & 0 deletions Cloud.API/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:13373",
"sslPort": 44310
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5291",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7297;http://localhost:5291",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
58 changes: 58 additions & 0 deletions Cloud.API/Services/EmployeeGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Bogus;
using Bogus.DataSets;
using Cloud.API.Models;
using Cloud.API.Services;

namespace Cloud.Api.Services;

/// <summary>
/// Генератор сотрудника по заданному id
/// </summary>
/// <param name="logger">Логгер</param>
public class EmployeeGenerator(
ILogger<EmployeeGenerator> logger
) : IEmployeeGenerator
{
private static readonly string[] _professions = { "Developer", "Manager", "Analyst", "Designer", "QA" };

private static readonly Dictionary<string, decimal> _baseSalaryBySuffix = new()
{
["Junior"] = 50000,
["Middle"] = 100000,
["Senior"] = 150000,
["Lead"] = 200000
Comment thread
alxmcs marked this conversation as resolved.
};

private readonly Faker<Employee> _faker = new Faker<Employee>("ru")
.RuleFor(e => e.Id, _ => 0)
.RuleFor(e => e.FullName, f =>
{
var gender = f.PickRandom<Name.Gender>();
return $"{f.Name.LastName(gender)} {f.Name.FirstName(gender)} " +
$"{f.Name.FirstName(Name.Gender.Male)}{(gender == Name.Gender.Male ? "ович" : "овна")}";

})
.RuleFor(e => e.Position, f => $"{f.PickRandom(_baseSalaryBySuffix.Keys.ToArray())} {f.PickRandom(_professions)}")
.RuleFor(e => e.Department, f => f.Commerce.Department())
.RuleFor(e => e.HireDate, f => DateOnly.FromDateTime(f.Date.Past(10)))
.RuleFor(e => e.Salary, (f, e) =>
{
var suffix = e.Position.Split(' ')[0];
var baseSalary = _baseSalaryBySuffix.GetValueOrDefault(suffix, 70000);
return Math.Round(baseSalary + f.Random.Decimal(-5000, 25000), 2);
})
.RuleFor(e => e.Email, (f, e) => f.Internet.Email(e.FullName))
.RuleFor(e => e.PhoneNumber, f => f.Phone.PhoneNumber("+7(###)###-##-##"))
.RuleFor(e => e.IsFired, f => f.Random.Bool(0.2f))
.RuleFor(e => e.FiredDate, (f, e) =>
e.IsFired ? DateOnly.FromDateTime(f.Date.Between(e.HireDate.ToDateTime(TimeOnly.MinValue), DateTime.Now)) : null);

/// <inheritdoc />
public Employee Generate(int id)
{
var employee = _faker.Generate();
employee.Id = id;
logger.LogInformation("Generated employee with id {employeeId}", id);
return employee;
}
}
86 changes: 86 additions & 0 deletions Cloud.API/Services/EmployeeService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Cloud.API.Models;
using Cloud.API.Services;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

namespace Cloud.Api.Services;

/// <summary>
/// Сервис для получения сотрудника компании по id с кэшированием
/// </summary>
/// <param name="generator">Генератор сотрудника</param>
/// <param name="cache">Сервис кэширования</param>
/// <param name="configuration">Конфигурация приложения</param>
/// <param name="logger">Логгер</param>
public class EmployeeService(
IEmployeeGenerator generator,
IDistributedCache cache,
IConfiguration configuration,
ILogger<EmployeeService> logger) : IEmployeeService
{
private readonly string _cacheKeyPrefix = configuration.GetValue("CacheKeyPrefix", "employee");
private readonly TimeSpan _cacheTtl = TimeSpan.FromMinutes(configuration.GetValue("CacheTtlMinutes", 30));

/// <inheritdoc />
public async Task<Employee> GetOrGenerateAsync(int id)
{
var cacheKey = $"{_cacheKeyPrefix}:{id}";

var cached = await GetFromCache(cacheKey);
if (cached is not null)
{
return cached;
}

logger.LogInformation("Cache miss for employee {Id}, generating new data", id);
var employee = generator.Generate(id);
await SetToCache(cacheKey, employee);
return employee;
}

/// <summary>
/// Метод получения сотрудника из кэша
/// </summary>
/// <param name="cacheKey">Ключ кэша</param>
/// <returns>
/// Сотрудник, или null в случае ошибки или отсутствия данных в кэше
/// </returns>
private async Task<Employee?> GetFromCache(string cacheKey)
{
try
{
var cached = await cache.GetStringAsync(cacheKey);
if (cached is null) return null;

logger.LogInformation("Cache hit for key {CacheKey}", cacheKey);
return JsonSerializer.Deserialize<Employee>(cached);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to read from cache for key {CacheKey}", cacheKey);
return null;
}
}

/// <summary>
/// Сохранение сотрудника в кэш с обработкой ошибок при записи
/// </summary>
/// <param name="cacheKey">Ключ кэша</param>
/// <param name="employee">Сотрудник компании</param>
private async Task SetToCache(string cacheKey, Employee employee)
{
try
{
var json = JsonSerializer.Serialize(employee);
await cache.SetStringAsync(cacheKey, json, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _cacheTtl
});
logger.LogInformation("Cached employee {Id} with key {CacheKey}", employee.Id, cacheKey);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to write to cache for key {CacheKey}", cacheKey);
}
}
}
15 changes: 15 additions & 0 deletions Cloud.API/Services/IEmployeeGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Cloud.API.Models;

namespace Cloud.API.Services;

/// <summary>
/// Интерфейс генератора сотрудника компании по id
/// </summary>
public interface IEmployeeGenerator
{
/// <summary>
/// Генерирует сотрудника компании с указанным id
/// </summary>
/// <param name="id">Идентификатор сотрудника компании</param>
public Employee Generate(int id);
}
Loading