Skip to content

arielsrv/valordolarhoy

Repository files navigation

Valor Dolar Hoy πŸ’°

.NET Code Coverage License .NET

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

  1. Clone the repository
git clone https://github.com/arielsrv/valordolarhoy.git
cd valordolarhoy
  1. Restore dependencies
dotnet restore
  1. Configure environment variables
# Create appsettings.Development.json or use environment variables
export ASPNETCORE_ENVIRONMENT=Development
export Storage__Redis=localhost:6379
  1. Run the application
dotnet run --project ValorDolarHoy
  1. 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

  1. Fork the project
  2. Create a feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. 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

πŸ“ž Contact


⭐ If this project helps you, give it a star!