Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graeme Hedges - Backend Task (Exchange Rate Updater .NET) #674

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8876f77
Minor: Added .vs to gitignore, updated ExchangeRateUpdater to .net8.0
GraemeHedges Jan 11, 2025
0d84379
Minor: Added test project to ExchangeRateUpdater
GraemeHedges Jan 11, 2025
f2a02ab
Minor: Updated ExchangeRateUpdater test project packages to latest st…
GraemeHedges Jan 11, 2025
967fa28
Minor: Updated ExchangeRateUpdater test project packages to latest st…
GraemeHedges Jan 11, 2025
c129ae0
Minor: Converted ExchangeRateUpdater to file scoped namespacing
GraemeHedges Jan 11, 2025
f8b3c83
Minor: Added failing test for ExchangeRateService
GraemeHedges Jan 11, 2025
f0d3871
Minor: Removed unused using across ExchangeRateUpdater, added Exchang…
GraemeHedges Jan 11, 2025
478da10
Minor: Adjusted ExchangeRateServiceTest to test call return type
GraemeHedges Jan 11, 2025
ed55393
Minor: Added code to make CallingExchangeRates pass
GraemeHedges Jan 11, 2025
d042795
Minor: Added global usings to ExchangeRateUpdater, added ExchangeRate…
GraemeHedges Jan 11, 2025
2d58aa5
Minor: Added setup to ExchangeRateServiceTests, added expected return
GraemeHedges Jan 11, 2025
e71f822
Minor: Hardcoded ExchangeRateServive to pass test
GraemeHedges Jan 11, 2025
eadf184
Minor: Renamed test project, added mocked http call to ExchangeRateSe…
GraemeHedges Jan 12, 2025
acbe8ca
Minor: Extracted logic from Program.cs to ExchangeRateUpdater (tempor…
GraemeHedges Jan 12, 2025
d5f3b7e
Minor: File scoped temp updater
GraemeHedges Jan 12, 2025
2d3aafa
Minor: Added basic HttpClient via factory to ExchangeRateService
GraemeHedges Jan 12, 2025
9709ff5
Minor: Removed paramaterless constructor from ExchangeRateService, ad…
GraemeHedges Jan 12, 2025
c50839b
Minor: Reduced ExchangeRateDTO to required properties only, added tes…
GraemeHedges Jan 12, 2025
99b2e5e
Handle non response status code in ExchangeRateService
GraemeHedges Jan 12, 2025
02d09d3
Minor: Added console logging, switched to typed HttpClientFactory pat…
GraemeHedges Jan 12, 2025
37e3f34
Minor: Removed usings, moved currency and exchangerate to models
GraemeHedges Jan 12, 2025
33996bc
Minor: Added TimeProvider to abstract time, changed call in ExchangeR…
GraemeHedges Jan 12, 2025
307c7be
Minor: Added amount back to Provider and adjusted tests
GraemeHedges Jan 12, 2025
148977a
Minor: Renamed DTOs to singular
GraemeHedges Jan 14, 2025
3dab86a
Minor: Implemented and moved ExchangeRateProvider
GraemeHedges Jan 14, 2025
7affe4c
Minor: Updated service, refactored return, added logging, caching onl…
GraemeHedges Jan 14, 2025
bae7401
Minor: Program.cs using new classes and returning more complete messa…
GraemeHedges Jan 14, 2025
1f9fc72
Minor: Added better variable names to linq query
GraemeHedges Jan 14, 2025
d3eca97
Minor: Fixed blank line
GraemeHedges Jan 15, 2025
17549c2
Minor: Added Readme, updated remaining files to filescoped namespaces…
GraemeHedges Jan 15, 2025
2d22686
Minor: Package updates
GraemeHedges Jan 15, 2025
e3a59d4
Minor: Updated Readme to include instructions on running the application
GraemeHedges Jan 15, 2025
3332e32
Minor: Removed restore step from instructions
GraemeHedges Jan 15, 2025
77272cd
Minor: Formatting changes for readme
GraemeHedges Jan 15, 2025
f6d6712
Minor: Fixed readme titles
GraemeHedges Jan 15, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
node_modules
bower_components
npm-debug.log
.vs
20 changes: 0 additions & 20 deletions jobs/Backend/Task/Currency.cs

This file was deleted.

15 changes: 15 additions & 0 deletions jobs/Backend/Task/DTO/ExchangeRateDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace ExchangeRateUpdater.DTOs;

public record ExchangeRateDTO
{
[JsonPropertyName("amount")]
public int Amount { get; init; }

[JsonPropertyName("currencyCode")]
public string CurrencyCode { get; init; }

[JsonPropertyName("rate")]
public decimal Rate { get; init; }
}
9 changes: 9 additions & 0 deletions jobs/Backend/Task/DTO/ExchangeRatesDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace ExchangeRateUpdater.DTOs;

public record ExchangeRatesDTO
{
[JsonPropertyName("rates")]
public IEnumerable<ExchangeRateDTO> Rates { get; init; }
}
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.

9 changes: 8 additions & 1 deletion jobs/Backend/Task/ExchangeRateUpdater.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.1.0" />
</ItemGroup>

</Project>
53 changes: 31 additions & 22 deletions jobs/Backend/Task/ExchangeRateUpdater.sln
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25123.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34309.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.Tests", "..\TestExchangeRateUpdater.Tests\ExchangeRateUpdater.Tests.csproj", "{D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.Build.0 = Release|Any CPU
{D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7EC3579-D063-4F8C-B3D3-5D1B6B287BE2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {26DF4BBA-AE0E-4E97-B657-B6406E4C09F2}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions jobs/Backend/Task/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using System.Collections.Generic;
19 changes: 19 additions & 0 deletions jobs/Backend/Task/Models/Currency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace ExchangeRateUpdater.Models;

public class Currency
{
public Currency(string code)
{
Code = code;
}

/// <summary>
/// Three-letter ISO 4217 code of the currency.
/// </summary>
public string Code { get; }

public override string ToString()
{
return Code;
}
}
22 changes: 22 additions & 0 deletions jobs/Backend/Task/Models/ExchangeRate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace ExchangeRateUpdater.Models;

public class ExchangeRate
{
public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value)
{
SourceCurrency = sourceCurrency;
TargetCurrency = targetCurrency;
Value = value;
}

public Currency SourceCurrency { get; }

public Currency TargetCurrency { get; }

public decimal Value { get; }

public override string ToString()
{
return $"{SourceCurrency}/{TargetCurrency}={Value}";
}
}
111 changes: 77 additions & 34 deletions jobs/Backend/Task/Program.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,86 @@
using System;
using System.Collections.Generic;
using ExchangeRateUpdater.Models;
using ExchangeRateUpdater.Providers;
using ExchangeRateUpdater.Services;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;

namespace ExchangeRateUpdater
var host = new HostBuilder()
.ConfigureServices(services =>
{
services.AddHttpClient<IExchangeRateService, ExchangeRateService>(
client =>
{
client.BaseAddress = new Uri("https://api.cnb.cz/cnbapi/");
})
.AddStandardResilienceHandler();
services.AddTransient<IExchangeRateProvider, ExchangeRateProvider>();
services.AddSingleton(TimeProvider.System);
services.AddSingleton<IMemoryCache, MemoryCache>();
services.AddLogging();
})
.ConfigureLogging(logging =>
{
logging.AddConsole();
})
.Build();

var exchangeRateProvider = host.Services.GetRequiredService<IExchangeRateProvider>();

IEnumerable<Currency> currencies = new[]
{
new Currency("USD"),
new Currency("EUR"),
new Currency("CZK"),
new Currency("JPY"),
new Currency("KES"),
new Currency("RUB"),
new Currency("THB"),
new Currency("TRY"),
new Currency("XYZ")
};

while (true)
{
public static class Program
try
{
private static IEnumerable<Currency> currencies = new[]
{
new Currency("USD"),
new Currency("EUR"),
new Currency("CZK"),
new Currency("JPY"),
new Currency("KES"),
new Currency("RUB"),
new Currency("THB"),
new Currency("TRY"),
new Currency("XYZ")
};
var rates = await exchangeRateProvider.GetExchangeRates(currencies);

public static void Main(string[] args)
{
try
{
var provider = new ExchangeRateProvider();
var rates = provider.GetExchangeRates(currencies);
if (!rates.Any())
{
Console.WriteLine(string.Empty);
Console.WriteLine("No exchange rates found for the chosen currencies.");
Console.WriteLine(string.Empty);
Console.WriteLine("Press any key to retry...");
Console.ReadKey();
Console.Clear();
continue;
}

Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:");
foreach (var rate in rates)
{
Console.WriteLine(rate.ToString());
}
}
catch (Exception e)
{
Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'.");
}
Console.WriteLine(string.Empty);
Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:");
Console.WriteLine(string.Empty);

Console.ReadLine();
foreach (var rate in rates)
{
Console.WriteLine(rate.ToString());
Console.WriteLine(string.Empty);
}

Console.WriteLine("Press any key to refresh...");
Console.ReadKey();
Console.Clear();
}
catch (Exception e)
{
Console.WriteLine(string.Empty);
Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'.");
Console.WriteLine(string.Empty);
Console.WriteLine("Press any key to retry...");
Console.ReadKey();
Console.Clear();
}
}
}
42 changes: 42 additions & 0 deletions jobs/Backend/Task/Providers/ExchangeRateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Threading.Tasks;
using ExchangeRateUpdater.DTOs;
using ExchangeRateUpdater.Models;
using ExchangeRateUpdater.Services;
using Microsoft.Extensions.Logging;

namespace ExchangeRateUpdater.Providers;

public class ExchangeRateProvider : IExchangeRateProvider
{
private readonly IExchangeRateService _exchangeRateService;
private readonly ILogger<ExchangeRateProvider> _logger;

public ExchangeRateProvider(IExchangeRateService exchangeRateService, ILogger<ExchangeRateProvider> logger)
{
_exchangeRateService = exchangeRateService;
_logger = logger;
}

/// <inheritdoc />
public async Task<IEnumerable<ExchangeRate>> GetExchangeRates(IEnumerable<Currency> currencies)
{
var exchangeRates = await _exchangeRateService.GetExchangeRatesAsync();

var filteredRates = exchangeRates.Rates
.Where(rate =>
currencies.Any(currency => currency.Code == rate.CurrencyCode))
.Select(rate =>
new ExchangeRate(new Currency(rate.CurrencyCode), new Currency("CZK"), rate.Rate / rate.Amount));

if (filteredRates.Count() is 0)
{
_logger.LogInformation("No matching rates found for chosen currency codes");
}

return filteredRates;
}
}
15 changes: 15 additions & 0 deletions jobs/Backend/Task/Providers/IExchangeRateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Threading.Tasks;
using ExchangeRateUpdater.Models;

namespace ExchangeRateUpdater.Providers;

public interface IExchangeRateProvider
{
/// <summary>
/// Should return exchange rates among the specified currencies that are defined by the source. But only those defined
/// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK",
/// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide
/// some of the currencies, ignore them.
/// </summary>
Task<IEnumerable<ExchangeRate>> GetExchangeRates(IEnumerable<Currency> currencies);
}
Loading