Skip to the content.

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

๐Ÿ—๏ธ Architecture

Technology Stack

Design Patterns

๐Ÿš€ Installation

Prerequisites

Local Development

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

๐Ÿค 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

๐Ÿ“„ 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!