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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@
node_modules
bower_components
npm-debug.log
/jobs/Backend/Task/.vs
/.vs
20 changes: 20 additions & 0 deletions jobs/Backend/Task/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

COPY ["ExchangeRates.Api/ExchangeRates.Api.csproj", "ExchangeRates.Api/"]
COPY ["ExchangeRates.Application/ExchangeRates.Application.csproj", "ExchangeRates.Application/"]
COPY ["ExchangeRates.Domain/ExchangeRates.Domain.csproj", "ExchangeRates.Domain/"]
COPY ["ExchangeRates.Infrastructure/ExchangeRates.Infrastructure.csproj", "ExchangeRates.Infrastructure/"]

RUN dotnet restore "ExchangeRates.Api/ExchangeRates.Api.csproj"

COPY . .
WORKDIR "/src/ExchangeRates.Api"
RUN dotnet publish "ExchangeRates.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .

EXPOSE 80
ENTRYPOINT ["dotnet", "ExchangeRates.Api.dll"]
19 changes: 0 additions & 19 deletions jobs/Backend/Task/ExchangeRateProvider.cs

This file was deleted.

8 changes: 0 additions & 8 deletions jobs/Backend/Task/ExchangeRateUpdater.csproj

This file was deleted.

22 changes: 0 additions & 22 deletions jobs/Backend/Task/ExchangeRateUpdater.sln

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using ExchangeRates.Api.DTOs;
using ExchangeRates.Application.Providers;
using ExchangeRates.Domain.Entities;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class ExchangeRatesController : ControllerBase
{
private readonly IExchangeRatesProvider _exchangeRatesProvider;

public ExchangeRatesController(IExchangeRatesProvider exchangeRatesProvider, ILogger<ExchangeRatesController> logger)
{
_exchangeRatesProvider = exchangeRatesProvider;
}

[HttpGet]
[ProducesResponseType(typeof(IEnumerable<ExchangeRate>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<IEnumerable<ExchangeRate>>> Get([FromQuery] GetExchangeRatesRequest request, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);

var currencies = request.Currencies ?? Array.Empty<string>();
var rates = await _exchangeRatesProvider.GetExchangeRatesAsync(currencies, cancellationToken);
return Ok(rates);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using ExchangeRates.Api.Dtos;

namespace ExchangeRates.Api.DTOs
{
public class GetExchangeRatesRequest
{
[ValidCurrencyCodes]
public IEnumerable<string>? Currencies { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;

namespace ExchangeRates.Api.Dtos
{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class ValidCurrencyCodesAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is IEnumerable<string> arr)
{
foreach (var c in arr)
{
if (string.IsNullOrWhiteSpace(c) || !Regex.IsMatch(c, "^[A-Za-z]{3}$"))
return new ValidationResult("All currency codes must be exactly 3 alphabetic characters.");
}
}
return ValidationResult.Success;
}
}
}
20 changes: 20 additions & 0 deletions jobs/Backend/Task/ExchangeRates.Api/ExchangeRates.Api.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

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

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.10" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

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

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace ExchangeRates.Api.Extensions
{
public static class ApplicationBuilderExtensions
{
public static WebApplication ConfigureApp(this WebApplication app, IHostEnvironment env)
{
app.UseExceptionHandler("/error");
app.UseCors("AllowAll");
app.UseRouting();

app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Exchange Rates API v1");
c.RoutePrefix = "swagger";
});

app.MapControllers();
app.Map("/error", (HttpContext httpContext) =>
{
return Results.Problem("An unexpected error occurred. Please try again later.");
});

return app;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using ExchangeRates.Application.Options;
using ExchangeRates.Application.Providers;
using ExchangeRates.Infrastructure.Clients.CNB;
using Microsoft.OpenApi.Models;

namespace ExchangeRates.Api.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddExchangeRatesServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddControllers();

services
.AddSwaggerDocumentation()
.AddCorsPolicy()
.AddCacheSupport()
.AddAppSettings(configuration)
.AddCNBClient(configuration)
.AddApplicationServices();

return services;
}

public static IServiceCollection AddAppSettings(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<ExchangeRatesOptions>(configuration.GetSection("ExchangeRates"));
services.Configure<CnbHttpClientOptions>(configuration.GetSection("CNBApi:HttpClient"));
return services;
}

public static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services)
{
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "Exchange Rates API",
Description = "API that provides daily exchange rates from the Czech National Bank."
});
});
return services;
}

public static IServiceCollection AddCorsPolicy(this IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
return services;
}

public static IServiceCollection AddCacheSupport(this IServiceCollection services)
{
services.AddDistributedMemoryCache();
return services;
}

public static IServiceCollection AddCNBClient(this IServiceCollection services, IConfiguration configuration)
{
var options = configuration
.GetSection("CNBApi:HttpClient")
.Get<CnbHttpClientOptions>()
?? new CnbHttpClientOptions();

if (options == null)
throw new InvalidOperationException("CNBApi configuration section is missing.");

if (string.IsNullOrWhiteSpace(options.BaseUrl))
throw new InvalidOperationException("CNBApi:HttpClient:BaseUrl configuration value is missing.");

services.AddHttpClient<ICnbHttpClient, CnbHttpClient>((serviceProvider, client) =>
{
client.BaseAddress = new Uri(options.BaseUrl);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddPolicyHandler((serviceProvider, request) =>
{
return CnbHttpClientPolicies.TimeoutPolicy(options);
})
.AddPolicyHandler((serviceProvider, request) =>
{
var logger = serviceProvider.GetRequiredService<ILogger<CnbHttpClient>>();
return CnbHttpClientPolicies.RetryPolicy(options, logger);
});

return services;
}

public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddScoped<IExchangeRatesProvider, ExchangeRatesProvider>();
return services;
}
}
}
24 changes: 24 additions & 0 deletions jobs/Backend/Task/ExchangeRates.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using ExchangeRates.Api.Extensions;

namespace ExchangeRates.Api
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
builder.Logging.AddConsole();

builder.Services.AddExchangeRatesServices(builder.Configuration);

var app = builder.Build();

app.ConfigureApp(builder.Environment);

app.Run();
}
}
}
31 changes: 31 additions & 0 deletions jobs/Backend/Task/ExchangeRates.Api/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:17427",
"sslPort": 44320
}
},
"profiles": {
"Development": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7253;http://localhost:5282",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"ExchangeRates": "Debug"
}
}
}
23 changes: 23 additions & 0 deletions jobs/Backend/Task/ExchangeRates.Api/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"Logging": {
"LogLevel": {
"Default": "Error",
"Microsoft": "Error",
"System": "Error",
"ExchangeRates": "Information"
}
},
"AllowedHosts": "*",
"CNBApi": {
"HttpClient": {
"BaseUrl": "https://api.cnb.cz/",
"TimeoutSeconds": 10,
"RetryCount": 2,
"RetryBaseDelaySeconds": 2,
"DailyRefreshTimeCZ": "14:30:00"
}
},
"ExchangeRates": {
"DefaultCurrencies": [ "USD", "EUR", "CZK", "JPY", "KES", "RUB", "THB", "TRY", "XYZ" ]
}
}
Loading