REST API to get the current dollar value in Argentina (official and blue) with reactive architecture using Rx.NET
Live Demo: https://valordolarhoy.herokuapp.com/
- π 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
- 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
- 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
- .NET 9.0 SDK
- Node.js 18+ (for frontend)
- Redis (optional, for production)
- 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 startdocker build -t valordolarhoy .
docker run -p 5000:5000 valordolarhoycurl https://valordolarhoy.herokuapp.com/Currencycurl https://valordolarhoy.herokuapp.com/Fallbackcurl https://valordolarhoy.herokuapp.com/Ping{
"official": {
"sell": 107.57,
"buy": 101.57
},
"blue": {
"sell": 200,
"buy": 196
}
}{
"code": 404,
"type": "ApiNotFoundException",
"message": "Not Found",
"detail": "The requested resource does not exist"
}{
"code": 500,
"type": "ApiException",
"message": "An internal server error has occurred",
"detail": "Please try again later"
}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
[HttpGet]
public async Task<IActionResult> GetLatestAsync()
{
return await TaskExecutor.ExecuteAsync(this.currencyService.GetLatest());
}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;
});
});
}services.AddHttpClient<ICurrencyClient, CurrencyClient>()
.SetTimeout(TimeSpan.FromMilliseconds(1500))
.SetMaxConnectionsPerServer(20)
.SetMaxParallelization(20);dotnet test ValorDolarHoy.Test/Unitdotnet test ValorDolarHoy.Test/Integration./coverage.sh[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);
}[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);
}The project includes GitHub Actions for:
- β Automatic build
- π§ͺ Test execution
- π Code coverage reporting
- π Automatic deployment to Heroku
- 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
- Follow C# and .NET conventions
- Add tests for new features
- Maintain high code coverage
- Document important changes
This project is licensed under the MIT License - see the LICENSE file for details.
- Rx.NET - Reactive Extensions for .NET
- AutoMapper - Object mapping
- Polly - Resilience and transient-fault-handling
- ServiceStack.Redis - Redis client
- Author: Ariel Servin
- GitHub: @arielsrv
- Demo: https://valordolarhoy.herokuapp.com/
β If this project helps you, give it a star!