202 lines
6.9 KiB
C#
202 lines
6.9 KiB
C#
using System.Diagnostics;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Serialization;
|
|
using ExpenseTracker.Application.Common.Exceptions;
|
|
using ExpenseTracker.Application.Common.Interfaces.Services;
|
|
using ExpenseTracker.Domain.Enums;
|
|
|
|
namespace ExpenseTracker.Infrastructure.Services;
|
|
|
|
public sealed class CurrencyConverterService : ICurrencyConverterService
|
|
{
|
|
private readonly ILogger<CurrencyConverterService> _logger;
|
|
private readonly HttpClient _httpClient;
|
|
private readonly string? _apiKey;
|
|
|
|
private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings()
|
|
{
|
|
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
|
};
|
|
|
|
private Dictionary<DateOnly, Dictionary<(Currency fromCurrency, Currency toCurrency), double>> exchangeRateCache;
|
|
|
|
public CurrencyConverterService(
|
|
ILogger<CurrencyConverterService> logger,
|
|
IHttpClientFactory httpClientFactory,
|
|
IConfiguration configuration)
|
|
{
|
|
_logger = logger;
|
|
_httpClient = httpClientFactory.CreateClient("CurrencyConverterService");
|
|
_apiKey = configuration.GetSection("FreeCurrencyAPI").GetValue<string?>("ApiKey");
|
|
|
|
InitializeCache();
|
|
}
|
|
|
|
public async Task<double> GetLatestExchangeRate(
|
|
Currency fromCurrency,
|
|
Currency toCurrency,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (fromCurrency.Equals(toCurrency))
|
|
{
|
|
return 1.0;
|
|
}
|
|
|
|
var currentDate = DateOnly.FromDateTime(DateTime.UtcNow);
|
|
|
|
if (TryGetExchangeRateFromCache(currentDate, fromCurrency, toCurrency, out double? result))
|
|
{
|
|
return (double)result;
|
|
}
|
|
|
|
HttpResponseMessage? response;
|
|
|
|
try
|
|
{
|
|
response = await _httpClient.GetAsync(
|
|
$"v1/latest" +
|
|
$"?apikey={_apiKey}" +
|
|
$"&base_currency={fromCurrency.ToString()}" +
|
|
$"¤cies={toCurrency.ToString()}",
|
|
cancellationToken);
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
}
|
|
catch (HttpRequestException exception)
|
|
{
|
|
_logger.LogError($"{exception.Message}\n{exception.StackTrace}");
|
|
throw new CurrencyConverterException(exception.Message);
|
|
}
|
|
|
|
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
|
|
_logger.LogDebug(
|
|
"{@DateUtc} {@TimeUtc} {@TraceId} {@SpanId} Received data: {@data}",
|
|
DateTime.UtcNow.ToString("yyyy-MM-dd"),
|
|
DateTime.UtcNow.ToString("HH:mm:ss.FFF"),
|
|
Activity.Current?.TraceId.ToString(),
|
|
Activity.Current?.SpanId.ToString(),
|
|
json);
|
|
|
|
dynamic deserealizedObject = JsonConvert.DeserializeObject(json);
|
|
|
|
var exchangeRate = (double)deserealizedObject["data"][toCurrency.ToString()];
|
|
|
|
AddToCache(currentDate, fromCurrency, toCurrency, exchangeRate);
|
|
|
|
return exchangeRate;
|
|
}
|
|
|
|
public async Task<double> GetHistoricalExchangeRate(
|
|
Currency fromCurrency,
|
|
Currency toCurrency,
|
|
DateOnly date,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
{
|
|
if (fromCurrency.Equals(toCurrency))
|
|
return 1.0;
|
|
}
|
|
|
|
if (TryGetExchangeRateFromCache(date, fromCurrency, toCurrency, out double? result))
|
|
{
|
|
return (double)result;
|
|
}
|
|
|
|
HttpResponseMessage? response;
|
|
|
|
try
|
|
{
|
|
response = await _httpClient.GetAsync(
|
|
$"v1/historical" +
|
|
$"?apikey={_apiKey}" +
|
|
$"&base_currency={fromCurrency.ToString()}" +
|
|
$"¤cies={toCurrency.ToString()}" +
|
|
$"&date={date.ToString("yyyy-MM-dd")}",
|
|
cancellationToken);
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
}
|
|
catch (HttpRequestException exception)
|
|
{
|
|
_logger.LogError($"{exception.Message}\n{exception.StackTrace}");
|
|
throw new CurrencyConverterException(exception.Message);
|
|
}
|
|
|
|
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
|
|
_logger.LogDebug(
|
|
"{@DateUtc} {@TimeUtc} {@TraceId} {@SpanId} Received data: {@data}",
|
|
DateTime.UtcNow.ToString("yyyy-MM-dd"),
|
|
DateTime.UtcNow.ToString("HH:mm:ss.FFF"),
|
|
Activity.Current?.TraceId.ToString(),
|
|
Activity.Current?.SpanId.ToString(),
|
|
json);
|
|
|
|
dynamic deserealizedObject = JsonConvert.DeserializeObject(json);
|
|
|
|
var exchangeRate = (double)deserealizedObject["data"][date.ToString("yyyy-MM-dd")][toCurrency.ToString()];
|
|
|
|
AddToCache(date, fromCurrency, toCurrency, exchangeRate);
|
|
|
|
return exchangeRate;
|
|
}
|
|
|
|
private void InitializeCache()
|
|
{
|
|
exchangeRateCache = new Dictionary<DateOnly, Dictionary<(Currency, Currency), double>>();
|
|
}
|
|
|
|
private void AddToCache(DateOnly date, Currency fromCurrency, Currency toCurrency, double exchangeRate)
|
|
{
|
|
if (!exchangeRateCache.ContainsKey(date))
|
|
{
|
|
var newDict = new Dictionary<(Currency, Currency), double>();
|
|
exchangeRateCache.Add(date, newDict);
|
|
}
|
|
|
|
if (!exchangeRateCache[date].ContainsKey((fromCurrency, toCurrency)))
|
|
{
|
|
exchangeRateCache[date].Add((fromCurrency, toCurrency), exchangeRate);
|
|
|
|
_logger.LogDebug(
|
|
"{@DateUtc} {@TimeUtc} {@TraceId} {@SpanId} Add data to cache: {@Date} {@FromCurrency} -> {@ToCurrency} {@ExchangeRate}",
|
|
DateTime.UtcNow.ToString("yyyy-MM-dd"),
|
|
DateTime.UtcNow.ToString("HH:mm:ss.FFF"),
|
|
Activity.Current?.TraceId.ToString(),
|
|
Activity.Current?.SpanId.ToString(),
|
|
date,
|
|
fromCurrency,
|
|
toCurrency,
|
|
exchangeRate);
|
|
}
|
|
}
|
|
|
|
private bool TryGetExchangeRateFromCache(DateOnly date, Currency fromCurrency, Currency toCurrency, out double? exchangeRate)
|
|
{
|
|
if (exchangeRateCache.ContainsKey(date) &&
|
|
exchangeRateCache[date].ContainsKey((fromCurrency, toCurrency)))
|
|
{
|
|
exchangeRate = exchangeRateCache[date][(fromCurrency, toCurrency)];
|
|
|
|
_logger.LogDebug(
|
|
"{@DateUtc} {@TimeUtc} {@TraceId} {@SpanId} Returned value from cache: {@Date} {@FromCurrency} -> {@ToCurrency} {@Value}",
|
|
DateTime.UtcNow.ToString("yyyy-MM-dd"),
|
|
DateTime.UtcNow.ToString("HH:mm:ss.FFF"),
|
|
Activity.Current?.TraceId.ToString(),
|
|
Activity.Current?.SpanId.ToString(),
|
|
date,
|
|
fromCurrency,
|
|
toCurrency,
|
|
exchangeRate);
|
|
|
|
return true;
|
|
}
|
|
|
|
exchangeRate = null;
|
|
return false;
|
|
}
|
|
}
|