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
20 changes: 0 additions & 20 deletions jobs/Backend/Task/Currency.cs

This file was deleted.

23 changes: 0 additions & 23 deletions jobs/Backend/Task/ExchangeRate.cs

This file was deleted.

19 changes: 0 additions & 19 deletions jobs/Backend/Task/ExchangeRateProvider.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using ExchangeRateUpdater.Application.Interfaces;
using ExchangeRateUpdater.Application.Validation;

namespace ExchangeRateUpdater.Api.Endpoints;

public static class ExchangeRateEndpoints
{
public static void MapExchangeRatesEndpoints(this WebApplication app)
{
app.MapGet("/exchange-rates", async (
string[] currencies,
IExchangeRateProvider exchangeRateProvider,
CancellationToken cancellationToken) =>
{
var validator = new ExchangeRatesRequestValidator();
var validationResult = await validator.ValidateAsync(currencies, cancellationToken);
if (!validationResult.IsValid)
{
return Results.BadRequest(validationResult.Errors.Select(e => e.ErrorMessage));
}

var result = await exchangeRateProvider.GetExchangeRates(currencies, cancellationToken);

return Results.Ok(result);
})
.WithName("GetExchangeRates")
.WithDescription("Gets exchange rates for provided currencies to CZK from Czech National Bank")
.WithTags("Exchange Rates");
}
}
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>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ExchangeRateUpdater.Application\ExchangeRateUpdater.Application.csproj" />
<ProjectReference Include="..\ExchangeRateUpdater.Infrastructure\ExchangeRateUpdater.Infrastructure.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@HostAddress = https://localhost:7232

### Get exchange rates for EUR and USD
GET {{HostAddress}}/exchange-rates?currencies=eur&currencies=usd
Accept: application/json
40 changes: 40 additions & 0 deletions jobs/Backend/Task/ExchangeRateUpdater.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using ExchangeRateUpdater.Api.Endpoints;
using ExchangeRateUpdater.Application.Configuration;
using ExchangeRateUpdater.Application.Interfaces;
using ExchangeRateUpdater.Application.Services;
using ExchangeRateUpdater.Infrastructure.HttpClients;
using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddScoped<IExchangeRateProvider, ExchangeRateProvider>();

builder.Services.Configure<ExchangeRateConfig>(
builder.Configuration.GetSection("ExchangeRateConfig"));

builder.Services.AddSingleton<ExchangeRateConfig>(sp =>
sp.GetRequiredService<IOptions<ExchangeRateConfig>>().Value);

builder.Services.AddHttpClient<IExchangeRateApiClient, ExchangeRateApiClient>((sp, client) =>
{
var config = sp.GetRequiredService<IOptions<ExchangeRateConfig>>().Value;
client.BaseAddress = new Uri(config.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(config.Timeout);
});

var app = builder.Build();

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

app.UseHttpsRedirection();

app.MapExchangeRatesEndpoints();

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger/index.html",
"applicationUrl": "https://localhost:7232;http://localhost:5028",
"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"
}
}
}
15 changes: 15 additions & 0 deletions jobs/Backend/Task/ExchangeRateUpdater.Api/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ExchangeRateConfig": {
"DefaultCurrency": "CZK",
"BaseUrl": "https://api.cnb.cz/cnbapi/",
"Timeout": 30,
"RatePrecision": 3
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace ExchangeRateUpdater.Application.Configuration;

public sealed class ExchangeRateConfig
{
public string BaseUrl { get; set; }

public string DefaultCurrency { get; set; }

public int Timeout { get; set; }

public int RatePrecision { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<Reference Include="Microsoft.Extensions.Logging.Abstractions">
<HintPath>..\..\..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.2\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Options">
<HintPath>..\..\..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.2\Microsoft.Extensions.Options.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentValidation" Version="12.1.1" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using ExchangeRateUpdater.Application.Models;

namespace ExchangeRateUpdater.Application.Interfaces;

public interface IExchangeRateApiClient
{
Task<IEnumerable<CnbExchangeRate>> GetAllExchangeRatesAsync(CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using ExchangeRateUpdater.Application.Models;

namespace ExchangeRateUpdater.Application.Interfaces;

public interface IExchangeRateProvider
{
Task<List<ExchangeRate>> GetExchangeRates(string[] currencies, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace ExchangeRateUpdater.Application.Models;

public sealed record CnbExchangeRate(string CurrencyCode, decimal Rate, int Amount);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace ExchangeRateUpdater.Application.Models;

public sealed record CnbExchangeRateResponse(CnbExchangeRate[] Rates);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace ExchangeRateUpdater.Application.Models;

public sealed record ExchangeRate(string SourceCurrency, string TargetCurrency, decimal Rate);
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using ExchangeRateUpdater.Application.Configuration;
using ExchangeRateUpdater.Application.Interfaces;
using ExchangeRateUpdater.Application.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ExchangeRateUpdater.Application.Services;

public sealed class ExchangeRateProvider : IExchangeRateProvider
{
private readonly IExchangeRateApiClient _apiClient;
private readonly ExchangeRateConfig _exchangeRateConfig;
private readonly ILogger<ExchangeRateProvider> _logger;

public ExchangeRateProvider(
IExchangeRateApiClient apiClient,
IOptions<ExchangeRateConfig> exchangeRateConfig,
ILogger<ExchangeRateProvider> logger)
{
_apiClient = apiClient;
_exchangeRateConfig = exchangeRateConfig.Value;
_logger = logger;
}

public async Task<List<ExchangeRate>> GetExchangeRates(string[] currencies, CancellationToken cancellationToken)
{
var cnbExchangeRates = await _apiClient.GetAllExchangeRatesAsync(cancellationToken);
return MapExchangeRates(cnbExchangeRates, currencies).ToList();
}

private IEnumerable<ExchangeRate> MapExchangeRates(
IEnumerable<CnbExchangeRate> cnbExchangeRates,
string[] currencyCodes)
{
var rateByCurrencyCode =
cnbExchangeRates.ToDictionary(x => x.CurrencyCode, StringComparer.OrdinalIgnoreCase);
foreach (var currencyCode in currencyCodes)
{
if (!rateByCurrencyCode.TryGetValue(currencyCode, out var exchangeRate))
{
_logger.LogWarning("No matches were found among CNB rates for currency '{@currencyCode}'", currencyCode);
continue;
}

yield return new(
exchangeRate.CurrencyCode,
_exchangeRateConfig.DefaultCurrency,
Math.Round(exchangeRate.Rate / exchangeRate.Amount, _exchangeRateConfig.RatePrecision));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using FluentValidation;

namespace ExchangeRateUpdater.Application.Validation;

public sealed class ExchangeRatesRequestValidator : AbstractValidator<string[]>
{
public ExchangeRatesRequestValidator()
{
RuleFor(x => x)
.NotEmpty()
.WithMessage("No currencies were provided as input");

RuleForEach(x => x)
.NotEmpty()
.WithMessage("Currency must not be empty")
.Matches("^[A-Za-z]{3}$")
.WithMessage("Each currency must be a 3-letter code. Example: USD");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<ProjectReference Include="..\ExchangeRateUpdater.Application\ExchangeRateUpdater.Application.csproj" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
<HintPath>..\..\..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.2\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Logging.Abstractions">
<HintPath>..\..\..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.2\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Options">
<HintPath>..\..\..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.2\Microsoft.Extensions.Options.dll</HintPath>
</Reference>
</ItemGroup>

</Project>
Loading