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
11 changes: 11 additions & 0 deletions jobs/Backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/.vs
/.vscode
/Task/.vs
/jobs/Backend/Task/.vscode
/jobs/Backend/Task/.vs
/jobs/Backend/Task/bin
/jobs/Backend/Task/obj
/jobs/Backend/Task/Properties
/jobs/Backend/Task/appsettings.json
/jobs/Backend/Task/appsettings.Development.json
/jobs/Backend/Task/appsettings.Production.json
27 changes: 27 additions & 0 deletions jobs/Backend/Task/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
**/.dockerignore
**/.env
**/.env.*
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
**/TestResults
**/coverage
LICENSE
README.md
20 changes: 0 additions & 20 deletions jobs/Backend/Task/Currency.cs

This file was deleted.

39 changes: 39 additions & 0 deletions jobs/Backend/Task/Dockerfile.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build

WORKDIR /src

COPY ["ExchangeRateUpdater.Api/ExchangeRateUpdater.Api.csproj", "ExchangeRateUpdater.Api/"]
COPY ["ExchangeRateUpdater.Console/ExchangeRateUpdater.Console.csproj", "ExchangeRateUpdater.Console/"]
COPY ["ExchangeRateUpdater.Domain/ExchangeRateUpdater.Domain.csproj", "ExchangeRateUpdater.Domain/"]
COPY ["ExchangeRateUpdater.Infrastructure/ExchangeRateUpdater.Infrastructure.csproj", "ExchangeRateUpdater.Infrastructure/"]

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

COPY . .

WORKDIR "/src/ExchangeRateUpdater.Api"
RUN dotnet build "ExchangeRateUpdater.Api.csproj" -c Release -o /app/build

WORKDIR "/src/ExchangeRateUpdater.Console"
RUN dotnet build "ExchangeRateUpdater.Console.csproj" -c Release -o /app/build

FROM build AS publish

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

WORKDIR "/src/ExchangeRateUpdater.Console"
RUN dotnet publish "ExchangeRateUpdater.Console.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .

ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:8080

ENTRYPOINT ["dotnet", "ExchangeRateUpdater.Api.dll"]
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,24 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace ExchangeRateUpdater.Api.Binders;

public class CommaSeparatedQueryBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();

if (string.IsNullOrWhiteSpace(value))
{
bindingContext.Result = ModelBindingResult.Success(new List<string>());
return Task.CompletedTask;
}

var values = value.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(v => v.Trim().ToUpperInvariant())
.ToList();

bindingContext.Result = ModelBindingResult.Success(values);
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Mvc;
using ExchangeRateUpdater.Api.Extensions;
using ExchangeRateUpdater.Api.Models;
using ExchangeRateUpdater.Domain.Models;
using ExchangeRateUpdater.Domain.Extensions;
using ExchangeRateUpdater.Api.Binders;

namespace ExchangeRateUpdater.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class ExchangeRatesController : ControllerBase
{
private readonly IExchangeRateService _exchangeRateService;

public ExchangeRatesController(IExchangeRateService exchangeRateService)
{
_exchangeRateService = exchangeRateService ?? throw new ArgumentNullException(nameof(exchangeRateService));
}

/// <summary>
/// Get exchange rates for specified currencies on specified date or closest business day if date is not provided.
/// </summary>
/// <param name="currencies">Comma-separated list of currency codes provided as a list of strings like [USD,EUR,JPY] or multiple currency parameters</param>
/// <param name="date">Optional date in YYYY-MM-DD format. Defaults to today if not present or if a future date is provided.</param>
/// <returns>Exchange rates for the specified currencies</returns>
/// <response code="200">Returns the exchange rates</response>
/// <response code="400">If the request is invalid</response>
/// <response code="404">If no exchange rates found for the specified currencies</response>
[HttpGet]
[ProducesResponseType(typeof(ApiResponse<ExchangeRateResponseDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponse<ExchangeRateResponseDto>>> GetExchangeRates(
[ModelBinder(BinderType = typeof(CommaSeparatedQueryBinder))] List<string> currencies,
[FromQuery] DateOnly? date = null)
{
var currencyObjects = ParseCurrencies(currencies);

var exchangeRates = await _exchangeRateService.GetExchangeRates(currencyObjects, date.AsMaybe());
if (!exchangeRates.Any())
{
var currencyList = string.Join(", ", currencyObjects.Select(c => c.Code));
return NotFound(ApiResponseBuilder.NotFound("No results found",
$"No exchange rates found for the specified currencies: {currencyList}"));
}

return Ok(ApiResponseBuilder.Success(
exchangeRates.ToExchangeRateResponse(date.AsMaybe()),
"Exchange rates retrieved successfully"));
}

private static IEnumerable<Currency> ParseCurrencies(List<string> currencies)
{
var currencyCodes = currencies.Select(code => code.Trim().ToUpperInvariant()).ToHashSet();

if (!currencyCodes.Any())
{
throw new ArgumentException("At least one currency code must be provided");
}

return currencyCodes.Select(code => new Currency(code));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
</ItemGroup>

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

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using ExchangeRateUpdater.Api.Models;
using ExchangeRateUpdater.Domain.Common;
using ExchangeRateUpdater.Domain.Models;

namespace ExchangeRateUpdater.Api.Extensions;

public static class ExchangeRateExtensions
{
public static ExchangeRateResponseDto ToExchangeRateResponse(
this IEnumerable<ExchangeRate> exchangeRates,
Maybe<DateOnly> requestedDate)
{
var rateList = exchangeRates.ToList();

return new ExchangeRateResponseDto(
Rates: rateList.Select(rate => new ExchangeRateDto(
SourceCurrency: rate.SourceCurrency.Code,
TargetCurrency: rate.TargetCurrency.Code,
Value: rate.Value,
Date: rate.Date.ToDateTime(TimeOnly.MinValue)
)).ToList(),
RequestedDate: requestedDate.TryGetValue(out var date) ? date.ToDateTime(TimeOnly.MinValue) : DateHelper.Today.ToDateTime(TimeOnly.MinValue),
TotalCount: rateList.Count
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using ExchangeRateUpdater.Infrastructure.Installers;
using ExchangeRateUpdater.Api.Middleware;

namespace ExchangeRateUpdater.Api.Extensions;

public static class ExchangeRateApiInstaller
{
public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddControllers();
services.AddOpenApiServices();
services.AddExchangeRateInfrastructure(configuration, useApiCache: true);
services.AddOpenTelemetry(configuration, "ExchangeRateUpdaterApi");

return services;
}

public static IServiceCollection AddOpenApiServices(this IServiceCollection services)
{
services.AddOpenApi();
services.AddSwaggerGen(options =>
{
var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename), includeControllerXmlComments: true);
});

return services;
}

public static IApplicationBuilder UseGlobalExceptionHandling(this IApplicationBuilder builder)
{
return builder.UseMiddleware<GlobalExceptionHandlingMiddleware>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Net;
using System.Text.Json;
using ExchangeRateUpdater.Domain.Common;
using ExchangeRateUpdater.Domain.Models;

namespace ExchangeRateUpdater.Api.Middleware;

public class GlobalExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionHandlingMiddleware> _logger;
private readonly IHostEnvironment _environment;

public GlobalExceptionHandlingMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionHandlingMiddleware> logger,
IHostEnvironment environment)
{
_next = next;
_logger = logger;
_environment = environment;
}

public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}

private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
_logger.LogError(exception, "An unhandled exception occurred.");

var response = context.Response;
response.ContentType = "application/json";

var apiResponse = new ApiResponse
{
Success = false,
Message = "An error occurred while processing your request",
Errors = new List<string>()
};

switch (exception)
{
case ExchangeRateProviderException:
response.StatusCode = (int)HttpStatusCode.BadGateway;
apiResponse.Message = "Exchange rate provider error";
apiResponse.Errors.Add(exception.Message);
break;

case ArgumentException:
response.StatusCode = (int)HttpStatusCode.BadRequest;
apiResponse.Message = "Invalid input";
apiResponse.Errors.Add(exception.Message);
break;

default:
response.StatusCode = (int)HttpStatusCode.InternalServerError;
apiResponse.Errors.Add(_environment.IsDevelopment()
? exception.ToString()
: "An unexpected error occurred. Please try again later.");
break;
}

var result = JsonSerializer.Serialize(apiResponse);
await response.WriteAsync(result);
}
}
Loading