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
33 changes: 33 additions & 0 deletions jobs/Backend/Task/.vs/ExchangeRateUpdater/xs/UserPrefs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Properties StartupConfiguration="{7B2695D6-D24C-4460-A58E-A10F08550CE0}|Default">
<MonoDevelop.Ide.Workbench ActiveDocument="Program.cs">
<Files>
<File FileName="../../../../../../usr/local/share/dotnet/sdk/7.0.317/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets" Line="148" Column="43" IsPinned="True" />
<File FileName="Program.cs" Line="1" Column="1" />
<File FileName="Config/AppConfig.cs" />
<File FileName="Models/Currency.cs" />
<File FileName="Models/ExchangeRate.cs" />
<File FileName="Services/CnbClient.cs" />
<File FileName="Services/CnbParser.cs" />
<File FileName="Services/ExchangeRateProvider.cs" />
<File FileName="appsettings.json" />
</Files>
<Pads>
<Pad Id="ProjectPad">
<State name="__root__">
<Node name="ExchangeRateUpdater">
<Node name="ExchangeRateUpdater">
<Node name="Program.cs" selected="True" />
</Node>
</Node>
</State>
</Pad>
</Pads>
</MonoDevelop.Ide.Workbench>
<MonoDevelop.Ide.ItemProperties.ExchangeRateUpdater PreferredExecutionTarget="MonoDevelop.Default" />
<MonoDevelop.Ide.DebuggingService.PinnedWatches />
<MultiItemStartupConfigurations />
<MonoDevelop.Ide.Workspace ActiveConfiguration="Debug" />
<MonoDevelop.Ide.DebuggingService.Breakpoints>
<BreakpointStore />
</MonoDevelop.Ide.DebuggingService.Breakpoints>
</Properties>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Format":1,"ProjectReferences":[],"MetadataReferences":[{"FilePath":"/Users/sachinshridhar/.nuget/packages/microsoft.extensions.configuration.abstractions/8.0.0/lib/net8.0/Microsoft.Extensions.Configuration.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sachinshridhar/.nuget/packages/microsoft.extensions.configuration.binder/8.0.2/lib/net8.0/Microsoft.Extensions.Configuration.Binder.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sachinshridhar/.nuget/packages/microsoft.extensions.configuration/8.0.0/lib/net8.0/Microsoft.Extensions.Configuration.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sachinshridhar/.nuget/packages/microsoft.extensions.configuration.fileextensions/8.0.0/lib/net8.0/Microsoft.Extensions.Configuration.FileExtensions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sachinshridhar/.nuget/packages/microsoft.extensions.configuration.json/8.0.0/lib/net8.0/Microsoft.Extensions.Configuration.Json.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sachinshridhar/.nuget/packages/microsoft.extensions.fileproviders.abstractions/8.0.0/lib/net8.0/Microsoft.Extensions.FileProviders.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sachinshridhar/.nuget/packages/microsoft.extensions.fileproviders.physical/8.0.0/lib/net8.0/Microsoft.Extensions.FileProviders.Physical.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sachinshridhar/.nuget/packages/microsoft.extensions.filesystemglobbing/8.0.0/lib/net8.0/Microsoft.Extensions.FileSystemGlobbing.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sachinshridhar/.nuget/packages/microsoft.extensions.primitives/8.0.0/lib/net8.0/Microsoft.Extensions.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sachinshridhar/.nuget/packages/system.text.encodings.web/8.0.0/lib/net8.0/System.Text.Encodings.Web.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sachinshridhar/.nuget/packages/system.text.json/8.0.0/lib/net8.0/System.Text.Json.dll","Aliases":[],"Framework":null}],"Files":["/Users/sachinshridhar/developers/jobs/Backend/Task/Config/AppConfig.cs","/Users/sachinshridhar/developers/jobs/Backend/Task/Models/Currency.cs","/Users/sachinshridhar/developers/jobs/Backend/Task/Models/ExchangeRate.cs","/Users/sachinshridhar/developers/jobs/Backend/Task/Program.cs","/Users/sachinshridhar/developers/jobs/Backend/Task/Services/CnbClient.cs","/Users/sachinshridhar/developers/jobs/Backend/Task/Services/CnbParser.cs","/Users/sachinshridhar/developers/jobs/Backend/Task/Services/ExchangeRateProvider.cs","/Users/sachinshridhar/developers/jobs/Backend/Task/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs","/Users/sachinshridhar/developers/jobs/Backend/Task/obj/Debug/net8.0/ExchangeRateUpdater.AssemblyInfo.cs","/Users/sachinshridhar/developers/jobs/Backend/Task/obj/Debug/net8.0/ExchangeRateUpdater.AssemblyInfo.cs","/Users/sachinshridhar/developers/jobs/Backend/Task/obj/Debug/net8.0/ExchangeRateUpdater.AssemblyInfo.cs","/Users/sachinshridhar/developers/jobs/Backend/Task/obj/Debug/net8.0/ExchangeRateUpdater.AssemblyInfo.cs"],"BuildActions":["Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile"],"Analyzers":["/usr/local/share/dotnet/sdk/7.0.317/Sdks/Microsoft.NET.Sdk/analyzers/Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll","/usr/local/share/dotnet/sdk/7.0.317/Sdks/Microsoft.NET.Sdk/analyzers/Microsoft.CodeAnalysis.NetAnalyzers.dll","/Users/sachinshridhar/.nuget/packages/system.text.json/8.0.0/analyzers/dotnet/roslyn4.4/cs/System.Text.Json.SourceGeneration.dll"],"AdditionalFiles":[],"EditorConfigFiles":["/Users/sachinshridhar/developers/jobs/Backend/Task/obj/Debug/net8.0/ExchangeRateUpdater.GeneratedMSBuildEditorConfig.editorconfig"],"DefineConstants":["TRACE","DEBUG","NET","NET8_0","NETCOREAPP","NET5_0_OR_GREATER","NET6_0_OR_GREATER","NET7_0_OR_GREATER","NETCOREAPP1_0_OR_GREATER","NETCOREAPP1_1_OR_GREATER","NETCOREAPP2_0_OR_GREATER","NETCOREAPP2_1_OR_GREATER","NETCOREAPP2_2_OR_GREATER","NETCOREAPP3_0_OR_GREATER","NETCOREAPP3_1_OR_GREATER"],"IntermediateAssembly":"/Users/sachinshridhar/developers/jobs/Backend/Task/obj/Debug/net8.0/ExchangeRateUpdater.dll"}
17 changes: 17 additions & 0 deletions jobs/Backend/Task/Config/AppConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ExchangeRateUpdater.Config;

public sealed class AppConfig
{
public ExchangeRateSettings ExchangeRateSettings { get; init; } = new();
}

public sealed class ExchangeRateSettings
{
[Required] public Uri CnbDailyUrl { get; init; } = default!;
[Range(1, 120)] public int HttpTimeoutSeconds { get; init; } = 10;
public List<string> Currencies { get; init; } = new();
}
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.

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

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

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />

<!-- HttpClientFactory -->
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />

<!-- In-memory cache (patched) -->
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
</ItemGroup>

<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>


<ItemGroup>
<None Remove="Models\" />
<None Remove="Services\" />
<None Remove="Config\" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models\" />
<Folder Include="Services\" />
<Folder Include="Config\" />
</ItemGroup>
</Project>
6 changes: 6 additions & 0 deletions jobs/Backend/Task/Models/Currency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ExchangeRateUpdater;

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

public sealed record ExchangeRate(Currency SourceCurrency, Currency TargetCurrency, decimal Value)
{
public override string ToString() => $"{SourceCurrency}/{TargetCurrency}={Value}";
}
110 changes: 76 additions & 34 deletions jobs/Backend/Task/Program.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,85 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using ExchangeRateUpdater;
using ExchangeRateUpdater.Config;
using ExchangeRateUpdater.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ExchangeRateUpdater
{
public static class Program
var host = Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(cfg =>
{
cfg.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
})
.ConfigureServices((context, services) =>
{
private static IEnumerable<Currency> currencies = new[]
var configuration = context.Configuration;

services.AddOptions<ExchangeRateSettings>()
.Bind(configuration.GetSection("ExchangeRateSettings"))
.ValidateDataAnnotations()
.Validate(s => s.CnbDailyUrl is not null && s.CnbDailyUrl.IsAbsoluteUri, "CnbDailyUrl must be absolute")
.Validate(s => s.CnbDailyUrl.Scheme == Uri.UriSchemeHttps, "CnbDailyUrl must be HTTPS")
.ValidateOnStart();

services.AddMemoryCache();

services.AddHttpClient<ICnbClient, CnbClient>((sp, client) =>
{
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")
};

public static void Main(string[] args)
var s = sp.GetRequiredService<IOptions<ExchangeRateSettings>>().Value;
client.Timeout = TimeSpan.FromSeconds(s.HttpTimeoutSeconds);
client.DefaultRequestHeaders.UserAgent.ParseAdd("ExchangeRateUpdater/1.0");
client.DefaultRequestHeaders.Accept.ParseAdd("text/plain");
client.BaseAddress = s.CnbDailyUrl;
});

services.AddSingleton<ICnbParser, CnbParser>();
services.AddSingleton<IExchangeRateProvider, ExchangeRateProvider>();
})
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
})
.Build();

using var scope = host.Services.CreateScope();
var sp = scope.ServiceProvider;
var logger = sp.GetRequiredService<ILogger<Program>>();

try
{
var settings = sp.GetRequiredService<IOptions<ExchangeRateSettings>>().Value;
var client = sp.GetRequiredService<ICnbClient>();
var provider = sp.GetRequiredService<IExchangeRateProvider>();

// Fetch payload once to detect decimal style used by the TXT feed.
var payload = await client.GetDailyRatesAsync(default);
var outputCulture = CnbParser.DetectCulture(payload);

// 2) currencies from config.
var currencies = settings.Currencies.Select(c => new Currency(c.Trim())).ToArray();
if (currencies.Length == 0)
{
logger.LogWarning("No currencies configured in appsettings.json.");
}
else
{
// Get per-unit rates and print with detected culture.
var rates = await provider.GetExchangeRatesAsync(currencies, default);
foreach (var r in rates)
{
try
{
var provider = new ExchangeRateProvider();
var rates = provider.GetExchangeRates(currencies);

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.ReadLine();
var formatted = r.Value.ToString("0.######", outputCulture);
Console.WriteLine($"{r.SourceCurrency}/{r.TargetCurrency}={formatted}");
}
}
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while fetching exchange rates.");
}
29 changes: 29 additions & 0 deletions jobs/Backend/Task/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Exchange Rate Updater (CNB Provider)

This is a .NET 8 console application that fetches daily exchange rates from the **Czech National Bank (CNB)** and prints the configured currency rates against CZK.

---

## 🚀 Features

- Fetches **real CNB daily rates** from official `.txt` feed.
- Automatically detects **decimal separator style** (`,` or `.`) from the file.
- Parses and computes **per-unit exchange rates** (`rate / amount`).
- Supports configurable currencies, timeout, and URL.
- Caches parsed data for faster repeated runs.
- Implements DI, logging, and options validation for production use.

---

## ⚙️ Configuration

All settings are defined in `appsettings.json`:

```json
{
"ExchangeRateSettings": {
"CnbDailyUrl": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt",
"HttpTimeoutSeconds": 10,
"Currencies": [ "EUR", "USD", "GBP", "INR" ]
}
}
48 changes: 48 additions & 0 deletions jobs/Backend/Task/Services/CnbClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ExchangeRateUpdater.Config;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ExchangeRateUpdater.Services;

public sealed class CnbClient : ICnbClient
{
private readonly HttpClient _http;
private readonly ExchangeRateSettings _settings;
private readonly ILogger<CnbClient> _logger;

public CnbClient(HttpClient http, IOptions<ExchangeRateSettings> options, ILogger<CnbClient> logger)
{
_http = http;
_settings = options.Value;
_logger = logger;
}

public async Task<string> GetDailyRatesAsync(CancellationToken ct)
{
try
{
using var resp = await _http.GetAsync("", HttpCompletionOption.ResponseHeadersRead, ct);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadAsStringAsync(ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
_logger.LogWarning("Fetching CNB daily rates was canceled.");
throw;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error fetching CNB daily rates from {Url}", _settings.CnbDailyUrl);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch CNB daily rates from {Url}", _settings.CnbDailyUrl);
throw;
}
}
}
Loading