Skip to content
Open
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,13 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp

vehicle-publish/
publish/
fileservice-publish/

*.zip

# Yandex Cloud — артефакты сборки и деплоя
cloud/build/
Client.Wasm/wwwroot/appsettings.Production.json
1 change: 1 addition & 0 deletions Client.Wasm/Client.Wasm.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<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.Extensions.Http" Version="10.0.5" />
</ItemGroup>

</Project>
7 changes: 3 additions & 4 deletions Client.Wasm/Components/DataCard.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@inject IConfiguration Configuration
@inject HttpClient Client
@using Client.Wasm
@inject VehicleApiClient ApiClient

<CardDeck>
<Card>
Expand Down Expand Up @@ -67,8 +67,7 @@

private async Task RequestNewData()
{
var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress");
Value = await Client.GetFromJsonAsync<JsonObject>($"{baseAddress}?id={Id}", new JsonSerializerOptions { });
Value = await ApiClient.GetVehicleAsync(Id);
StateHasChanged();
}
}
12 changes: 6 additions & 6 deletions Client.Wasm/Components/StudentCard.razor
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<Card>
<Card>
<CardHeader>
<Heading Size="HeadingSize.Is5"><Icon Name="IconName.User" /> Лабораторная работа</Heading>
<Heading Size="HeadingSize.Is5"><Icon Name="IconName.Cloud" /> Лабораторная работа</Heading>
</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>№4 "Переход на облачную инфраструктуру"</Strong></UnorderedListItem>
<UnorderedListItem>Вариант <Strong>№29 "Транспортное средство"</Strong></UnorderedListItem>
<UnorderedListItem>Выполнена <Strong>Нестеренко Андреем 6512</Strong></UnorderedListItem>
<UnorderedListItem><Link To="https://github.com/MagGoldi/cloud-development">Ссылка на репозиторий</Link></UnorderedListItem>
</UnorderedList>
</CardBody>
</Card>
7 changes: 6 additions & 1 deletion Client.Wasm/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddHttpClient<VehicleApiClient>(client =>
{
var baseAddress = builder.Configuration["BaseAddress"]
?? throw new InvalidOperationException("BaseAddress not found in configuration.");
client.BaseAddress = new Uri(baseAddress);
});
builder.Services.AddBlazorise(options => { options.Immediate = true; })
.AddBootstrapProviders()
.AddFontAwesomeIcons();
Expand Down
12 changes: 12 additions & 0 deletions Client.Wasm/VehicleApiClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Net.Http.Json;
using System.Text.Json.Nodes;

namespace Client.Wasm;

public class VehicleApiClient(HttpClient httpClient)
{
public async Task<JsonObject?> GetVehicleAsync(int id)
{
return await httpClient.GetFromJsonAsync<JsonObject>($"api/vehicle/{id}");
}
}
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": "http://localhost:5200/"
}
57 changes: 55 additions & 2 deletions CloudDevelopment.sln
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36811.4
VisualStudioVersion = 17.13.35931.197
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}") = "ProjectApp.Api", "ProjectApp.Api\ProjectApp.Api.csproj", "{E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.Domain", "ProjectApp.Domain\ProjectApp.Domain.csproj", "{CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.ServiceDefaults", "ProjectApp.ServiceDefaults\ProjectApp.ServiceDefaults.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.AppHost", "ProjectApp.AppHost\ProjectApp.AppHost.csproj", "{2A5FB573-9376-4FEB-9289-A8387F435C13}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.Gateway", "ProjectApp.Gateway\ProjectApp.Gateway.csproj", "{EBCDE049-6A3B-431E-ACBA-DD9C90898F49}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.FileService", "ProjectApp.FileService\ProjectApp.FileService.csproj", "{FBAF0E91-832A-4594-9515-C9D42EA8A25B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.Tests", "ProjectApp.Tests\ProjectApp.Tests.csproj", "{38936714-96E7-4B0C-904A-0B01D8E96B2D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.Api.Function", "ProjectApp.Api.Function\ProjectApp.Api.Function.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.FileService.Function", "ProjectApp.FileService.Function\ProjectApp.FileService.Function.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F23456789012}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +32,42 @@ 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
{E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|Any CPU.Build.0 = Release|Any CPU
{CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Release|Any CPU.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
{2A5FB573-9376-4FEB-9289-A8387F435C13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A5FB573-9376-4FEB-9289-A8387F435C13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A5FB573-9376-4FEB-9289-A8387F435C13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A5FB573-9376-4FEB-9289-A8387F435C13}.Release|Any CPU.Build.0 = Release|Any CPU
{EBCDE049-6A3B-431E-ACBA-DD9C90898F49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EBCDE049-6A3B-431E-ACBA-DD9C90898F49}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBCDE049-6A3B-431E-ACBA-DD9C90898F49}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBCDE049-6A3B-431E-ACBA-DD9C90898F49}.Release|Any CPU.Build.0 = Release|Any CPU
{FBAF0E91-832A-4594-9515-C9D42EA8A25B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FBAF0E91-832A-4594-9515-C9D42EA8A25B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FBAF0E91-832A-4594-9515-C9D42EA8A25B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FBAF0E91-832A-4594-9515-C9D42EA8A25B}.Release|Any CPU.Build.0 = Release|Any CPU
{38936714-96E7-4B0C-904A-0B01D8E96B2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{38936714-96E7-4B0C-904A-0B01D8E96B2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{38936714-96E7-4B0C-904A-0B01D8E96B2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{38936714-96E7-4B0C-904A-0B01D8E96B2D}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
145 changes: 145 additions & 0 deletions ProjectApp.Api.Function/Handler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Amazon.Runtime;
using Amazon.SQS;
using Amazon.SQS.Model;

namespace ApiFunction;

/// <summary>
/// Cloud Function генерации транспортного средства по идентификатору.
/// При наличии кэша в Object Storage возвращает сохранённые данные;
/// иначе генерирует новые и публикует их в Message Queue.
/// </summary>
public class Handler
{
private static readonly Random _rng = Random.Shared;
private static readonly HttpClient _http = new();

private static readonly string[] _brands = ["Toyota", "BMW", "Mercedes", "Audi", "Ford", "Honda", "Volkswagen", "Hyundai", "Kia", "Nissan"];
private static readonly string[] _models = ["Sedan", "SUV", "Hatchback", "Coupe", "Crossover", "Pickup", "Minivan", "Wagon"];
private static readonly string[] _bodyTypes = ["Sedan", "SUV", "Hatchback", "Coupe", "Crossover"];
private static readonly string[] _fuelTypes = ["Petrol", "Diesel", "Electric", "Hybrid"];
private static readonly string[] _colors = ["White", "Black", "Silver", "Blue", "Red", "Grey", "Green"];

private static readonly JsonSerializerOptions _camelCase = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

/// <summary>
/// Точка входа Cloud Function. Принимает HTTP-запрос от API Gateway,
/// возвращает JSON транспортного средства с CORS-заголовками.
/// </summary>
/// <param name="input">Сериализованный запрос от API Gateway</param>
public string FunctionHandler(string input)
{
FunctionRequest? req = null;
try { req = JsonSerializer.Deserialize<FunctionRequest>(input ?? "{}"); } catch { }

string? idRaw = null;
req?.PathParams?.TryGetValue("id", out idRaw);
if (string.IsNullOrEmpty(idRaw))
{
var url = req?.Url ?? req?.Path ?? "";
idRaw = url.Split('?')[0].Split('/', StringSplitOptions.RemoveEmptyEntries)
.LastOrDefault(p => !p.StartsWith('{'));
}

if (!int.TryParse(idRaw, out var id) || id <= 0)
return Envelope(400, """{"error":"Identifier must be a positive number."}""");

var cached = TryGetCachedAsync(id).GetAwaiter().GetResult();
if (cached != null)
return Envelope(200, cached);

var vehicle = new
{
id,
vin = GenerateVin(),
brand = _brands[_rng.Next(_brands.Length)],
model = _models[_rng.Next(_models.Length)],
year = _rng.Next(2010, 2025),
bodyType = _bodyTypes[_rng.Next(_bodyTypes.Length)],
fuelType = _fuelTypes[_rng.Next(_fuelTypes.Length)],
color = _colors[_rng.Next(_colors.Length)],
mileage = Math.Round(_rng.NextDouble() * 200000, 1),
lastServiceDate = DateTime.UtcNow.AddDays(-_rng.Next(30, 730)).ToString("yyyy-MM-dd")
};

var json = JsonSerializer.Serialize(vehicle, _camelCase);

Task.Run(() => TryPublishAsync(json));

return Envelope(200, json);
}

private static string Envelope(int status, string body) =>
JsonSerializer.Serialize(new
{
statusCode = status,
headers = new Dictionary<string, string>
{
["Content-Type"] = "application/json",
["Access-Control-Allow-Origin"] = "*"
},
body
});

private static string GenerateVin()
{
const string chars = "ABCDEFGHJKLMNPRSTUVWXYZ0123456789";
return string.Concat(Enumerable.Range(0, 17).Select(_ => chars[_rng.Next(chars.Length)]));
}

private static async Task<string?> TryGetCachedAsync(int id)
{
try
{
var bucket = Environment.GetEnvironmentVariable("S3_BUCKET") ?? "vehicle-data-store";
var url = $"https://storage.yandexcloud.net/{bucket}/vehicle-{id}.json";
var resp = await _http.GetAsync(url);
if (resp.IsSuccessStatusCode)
return await resp.Content.ReadAsStringAsync();
}
catch { }
return null;
}

private static async Task TryPublishAsync(string messageBody)
{
try
{
var queueUrl = Environment.GetEnvironmentVariable("SQS_QUEUE_URL") ?? "";
var accessKey = Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID") ?? "";
var secretKey = Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY") ?? "";
if (string.IsNullOrEmpty(queueUrl) || string.IsNullOrEmpty(accessKey)) return;

var credentials = new BasicAWSCredentials(accessKey, secretKey);
var config = new AmazonSQSConfig
{
ServiceURL = "https://message-queue.api.cloud.yandex.net",
AuthenticationRegion = "ru-central1"
};
using var sqs = new AmazonSQSClient(credentials, config);
await sqs.SendMessageAsync(new SendMessageRequest { QueueUrl = queueUrl, MessageBody = messageBody });
}
catch (Exception ex)
{
Console.WriteLine($"[WARN] SQS publish failed: {ex.Message}");
}
}
}

/// <summary>
/// Модель входящего запроса от Yandex API Gateway.
/// </summary>
public class FunctionRequest
{
[JsonPropertyName("httpMethod")] public string HttpMethod { get; set; } = "";
[JsonPropertyName("headers")] public Dictionary<string, string>? Headers { get; set; }
[JsonPropertyName("path")] public string? Path { get; set; }
[JsonPropertyName("queryStringParameters")] public Dictionary<string, string>? QueryStringParameters { get; set; }
[JsonPropertyName("pathParameters")] public Dictionary<string, string>? PathParameters { get; set; }
[JsonPropertyName("pathParams")] public Dictionary<string, string>? PathParams { get; set; }
[JsonPropertyName("url")] public string? Url { get; set; }
[JsonPropertyName("body")] public string? Body { get; set; }
[JsonPropertyName("isBase64Encoded")] public bool IsBase64Encoded { get; set; }
}
42 changes: 42 additions & 0 deletions ProjectApp.Api.Function/Models/FunctionModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Text.Json.Serialization;

namespace ApiFunction.Models;

public class FunctionRequest
{
[JsonPropertyName("httpMethod")]
public string HttpMethod { get; set; } = "";

[JsonPropertyName("headers")]
public Dictionary<string, string>? Headers { get; set; }

[JsonPropertyName("path")]
public string? Path { get; set; }

[JsonPropertyName("queryStringParameters")]
public Dictionary<string, string>? QueryStringParameters { get; set; }

[JsonPropertyName("pathParameters")]
public Dictionary<string, string>? PathParameters { get; set; }

[JsonPropertyName("body")]
public string? Body { get; set; }

[JsonPropertyName("isBase64Encoded")]
public bool IsBase64Encoded { get; set; }
}

public class FunctionResponse
{
[JsonPropertyName("statusCode")]
public int StatusCode { get; set; }

[JsonPropertyName("headers")]
public Dictionary<string, string>? Headers { get; set; }

[JsonPropertyName("body")]
public string Body { get; set; } = "";

[JsonPropertyName("isBase64Encoded")]
public bool IsBase64Encoded { get; set; }
}
42 changes: 42 additions & 0 deletions ProjectApp.Api.Function/ModuleInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Reflection;
using System.Runtime.CompilerServices;

namespace ApiFunction;

/// <summary>
/// Инициализатор модуля — регистрирует обработчик AssemblyResolve до обращения к любому классу.
/// </summary>
public static class ModuleInit
{
[ModuleInitializer]
public static void Initialize()
{
AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
}

private static Assembly? ResolveAssembly(object? sender, ResolveEventArgs args)
{
try
{
var name = new AssemblyName(args.Name).Name;
if (string.IsNullOrEmpty(name)) return null;

var assemblyDir = Path.GetDirectoryName(typeof(ModuleInit).Assembly.Location) ?? "";
var dllPath = Path.Combine(assemblyDir, name + ".dll");

if (File.Exists(dllPath))
{
Console.WriteLine($"[ModuleInit] Resolved {name} from {dllPath}");
return Assembly.LoadFrom(dllPath);
}

Console.WriteLine($"[ModuleInit] Could not resolve {name} (looked in {assemblyDir})");
}
catch (Exception ex)
{
Console.WriteLine($"[ModuleInit] ResolveAssembly error: {ex.Message}");
}

return null;
}
}
Loading
Loading