Valor Dolar Hoy ๐ฐ
REST API to get the current dollar value in Argentina (official and blue) with reactive architecture using Rx.NET
๐ Demo
Live Demo: https://valordolarhoy.herokuapp.com/
๐ Features
- ๐ Reactive Architecture: Uses Rx.NET for asynchronous and reactive handling
- ๐พ Smart Caching: In-memory and Redis caching system for performance optimization
- ๐ก๏ธ Resilience: Implements Polly for retry policies and circuit breakers
- ๐ Monitoring: Health checks and automatic warmup
- ๐งช Complete Testing: Unit tests, integration tests and code coverage
- ๐ Performance: Optimized for high concurrency
- ๐ฑ React Frontend: Modern interface with React and TypeScript
๐๏ธ Architecture
Technology Stack
- Backend: ASP.NET Core 9.0, C# 13
- Frontend: React 18, TypeScript
- Reactive Programming: Rx.NET
- Caching: Redis + Memory Cache
- Testing: xUnit, Moq
- Deployment: Docker, Heroku
Design Patterns
- Reactive Programming: Observable patterns with Rx.NET
- Repository Pattern: For data access
- Dependency Injection: Native IoC container
- Circuit Breaker: For resilience in external calls
- Caching Strategy: Multi-layer caching
๐ Installation
Prerequisites
- .NET 9.0 SDK
- Node.js 18+ (for frontend)
- Redis (optional, for production)
Local Development
- Clone the repository
git clone https://github.com/arielsrv/valordolarhoy.git cd valordolarhoy
- Restore dependencies
dotnet restore
- Configure environment variables
# Create appsettings.Development.json or use environment variables export ASPNETCORE_ENVIRONMENT=Development export Storage__Redis=localhost:6379
- Run the application
dotnet run --project ValorDolarHoy
- Frontend (optional)
cd ValorDolarHoy/ClientApp npm install npm start
Docker
docker build -t valordolarhoy .
docker run -p 5000:5000 valordolarhoy
๐ Usage
API Endpoints
Get Current Exchange Rate
curl https://valordolarhoy.herokuapp.com/Currency
Get Exchange Rate with Fallback
curl https://valordolarhoy.herokuapp.com/Fallback
Health Check
curl https://valordolarhoy.herokuapp.com/Ping
Responses
โ 200 - Successful Exchange Rate
{
"official": {
"sell": 107.57,
"buy": 101.57
},
"blue": {
"sell": 200,
"buy": 196
}
}
โ 404 - Resource Not Found
{
"code": 404,
"type": "ApiNotFoundException",
"message": "Not Found",
"detail": "The requested resource does not exist"
}
โ 500 - Internal Error
{
"code": 500,
"type": "ApiException",
"message": "An internal server error has occurred",
"detail": "Please try again later"
}
๐ง Development
Project Structure
ValorDolarHoy/
โโโ ValorDolarHoy.Core/ # Main backend
โ โโโ Controllers/ # API Controllers
โ โโโ Services/ # Business Logic
โ โโโ Clients/ # External API clients
โ โโโ Common/ # Shared utilities
โ โโโ Middlewares/ # Custom middlewares
โโโ ValorDolarHoy/ # Web application
โ โโโ ClientApp/ # React frontend
โโโ ValorDolarHoy.Test/ # Test projects
โโโ Unit/ # Unit tests
โโโ Integration/ # Integration tests
Code Examples
Reactive Controller
[HttpGet]
public async Task<IActionResult> GetLatestAsync()
{
return await TaskExecutor.ExecuteAsync(this.currencyService.GetLatest());
}
Service with Cache and Fallback
public IObservable<CurrencyDto> GetFallback()
{
string cacheKey = GetCacheKey();
return this.keyValueStore.Get<CurrencyDto>(cacheKey).FlatMap(currencyDto =>
{
return currencyDto != null
? Observable.Return(currencyDto)
: this.GetFromApi().Map(response =>
{
this.executorService.Run(() =>
this.keyValueStore.Put(cacheKey, response, 60 * 10).ToBlocking());
return response;
});
});
}
Configured HTTP Client
services.AddHttpClient<ICurrencyClient, CurrencyClient>()
.SetTimeout(TimeSpan.FromMilliseconds(1500))
.SetMaxConnectionsPerServer(20)
.SetMaxParallelization(20);
Testing
Unit Tests
dotnet test ValorDolarHoy.Test/Unit
Integration Tests
dotnet test ValorDolarHoy.Test/Integration
Code Coverage
./coverage.sh
๐งช Testing
Unit Test Example
[Fact]
public void Get_Latest_Ok_Fallback_FromApi()
{
this.keyValueStore.Setup(store => store.Get<CurrencyDto>("bluelytics:v1"))
.Returns(Observable.Return(default(CurrencyDto)));
this.currencyClient.Setup(client => client.Get()).Returns(GetLatest());
CurrencyService currencyService = new(this.currencyClient.Object, this.keyValueStore.Object);
CurrencyDto currencyDto = currencyService.GetFallback().ToBlocking();
Assert.NotNull(currencyDto);
Assert.Equal(10.0M, currencyDto.Official!.Buy);
Assert.Equal(11.0M, currencyDto.Official.Sell);
Assert.Equal(12.0M, currencyDto.Blue!.Buy);
Assert.Equal(13.0M, currencyDto.Blue.Sell);
}
Integration Test Example
[Fact]
public async Task Basic_Integration_Test_InternalServerErrorAsync()
{
this.currencyService.Setup(service => service.GetLatest())
.Returns(Observable.Throw<CurrencyDto>(new ApiException()));
HttpResponseMessage httpResponseMessage = await this.httpClient.GetAsync("/Currency");
string responseString = await httpResponseMessage.Content.ReadAsStringAsync();
ErrorHandlerMiddleware.ErrorModel? errorModel = JsonConvert
.DeserializeObject<ErrorHandlerMiddleware.ErrorModel>(responseString);
Assert.NotNull(errorModel);
Assert.Equal(500, errorModel.Code);
Assert.Equal(nameof(ApiException), errorModel.Type);
}
๐ CI/CD
The project includes GitHub Actions for:
- โ Automatic build
- ๐งช Test execution
- ๐ Code coverage reporting
- ๐ Automatic deployment to Heroku
๐ค Contributing
- Fork the project
- Create a feature branch (
git checkout -b feature/AmazingFeature
) - Commit your changes (
git commit -m 'Add some AmazingFeature'
) - Push to the branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
Contribution Guidelines
- Follow C# and .NET conventions
- Add tests for new features
- Maintain high code coverage
- Document important changes
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Acknowledgments
- Rx.NET - Reactive Extensions for .NET
- AutoMapper - Object mapping
- Polly - Resilience and transient-fault-handling
- ServiceStack.Redis - Redis client
๐ Contact
- Author: Ariel Servin
- GitHub: @arielsrv
- Demo: https://valordolarhoy.herokuapp.com/
โญ If this project helps you, give it a star!