App-Idea 5: Currency Converter

Our next app will be a Currency Converter app that allows users to enter a monetary amount and currency and then converts that amount to another currency. This conversion app uses a free currency web service to find the list of available currencies, and the conversion rates between any two currencies. We will learn how to use the HttpClient to send messages to web services and receive responses. Then, we will use System.Text.Json to convert JSON responses into our model classes. Finally, we add a Services layer to our typical MVVM design pattern to encapsulate the web service integration.

We will add this app as a new page to the existing Blazor.AppIdeas.Converters app (rather than creating a separate full project). You can find the source for the Blazor.AppIdeas.Converters on GitHub. And the running sample of the Blazor app online.

We will start by loading Blazor.AppIdeas.Converters solution (created in App-Idea 1: Bin2Dec) into Visual Studio.

Model Classes

Our model classes are partially designed with the currency web service in mind. We will use the data retrieved from that web service to populate the properties of our model classes. Let’s start by creating the CurrencyDescriptor class in the Blazor.AppIdeas.Converters project and Models folder.

namespace Blazor.AppIdeas.Converters.Models
{
    public class CurrencyDescriptor
    {
        public string Id { get; set; }

        public string CurrencyName { get; set; }

        public string CurrencySymbol { get; set; }
    }
}

This class holds descriptive information about our supported currencies. This data will be retrieved from the currency web service and listed in our user interface. The Id is the universal short name for a currency, the CurrencyName is the friendly display name, and the CurrencySymbol is the single character representation of the currency (if it has one), like the $ symbol.

Now, let’s create the CurrencyConversionRate model class in the Blazor.AppIdeas.Converters project and Models folder.

using System;

namespace Blazor.AppIdeas.Converters.Models
{
    public class CurrencyConversionRate
    {
        public CurrencyConversionRate(string convertFrom, string convertTo, decimal value)
        {
            if (string.IsNullOrEmpty(convertFrom)) throw new ArgumentNullException(nameof(convertFrom));
            if (string.IsNullOrEmpty(convertTo)) throw new ArgumentNullException(nameof(convertTo));
            if (value < 0M) throw new ArgumentOutOfRangeException(nameof(value));

            ConvertFrom = convertFrom;
            ConvertTo = convertTo;
            Value = value;
        }
        public string ConvertFrom { get; }

        public string ConvertTo { get; }

        public decimal Value { get; }
    }
}

This class represents the response message from the conversion web service call. It contains the ids of the source and destination currencies and the conversion rate.

Service Client Class to Communicate with Web Service

We will create a service client class to encapsulate our calls to the currency conversion web service. This will be the only class that needs to know about the web service, its URIs, additional data (like api keys), and logic for converting JSON responses to our model classes. This layer is a great addition to the MVVM design pattern because it separates our application logic from our data access / service calls.

We will also introduce a service client interface to help with the testability and resiliency of our system. We can have multiple implementations of the interface: one for our web service calls, and one for our test mock object (so our unit tests are not dependent on connecting to a live web service). This will allow our tests to run faster and more reliably.

First, we will define the ICurrencyServiceClient interface in the Blazor.AppIdeas.Converters project and Services folder (this folder doesn’t exist yet, so let’s create that too).

using Blazor.AppIdeas.Converters.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Blazor.AppIdeas.Converters.Services
{
    public interface ICurrencyServiceClient
    {
        Task<CurrencyConversion> GetConversionRate(string convertFromId, string convertToId);

        Task<IEnumerable<CurrencyDescriptor>> GetCurrencies();
    }
}

This interface defines two asynchronous methods. Since they represent calls to an external web service, we want them asynchronous, so that we don’t make our application non-responsive while we are fetching the data.

  • The GetCurrencies method takes no parameters and returns a list of CurrencyDescriptor model objects. These would represent the currency types supported by this service.
  • The GetConversionRate method takes the source and destination currency ids and returns the CurrencyConversion object for that currency pair.

Then, we create the CurrencyServiceClient class that implements this interface, in the same folder.

using Blazor.AppIdeas.Converters.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace Blazor.AppIdeas.Converters.Services
{
    public class CurrencyServiceClient : ICurrencyServiceClient
    {
        private readonly string _serviceCurrencyUrl =
            "https://free.currconv.com/api/v7/currencies?apiKey=e49bfc9cd7ad694b2dd9";
        private readonly string _serviceConvertUrl =
            "https://free.currconv.com/api/v7/convert?apiKey=e49bfc9cd7ad694b2dd9&compact=y";

        private readonly HttpClient _httpClient;

        public CurrencyServiceClient(HttpClient http)
        {
            _httpClient = http ?? throw new ArgumentNullException(nameof(http));
        }

        public CurrencyServiceClient(HttpClient http, string currencyUrl, string convertUrl)
            : this(http)
        {
            _serviceCurrencyUrl = currencyUrl;
            _serviceConvertUrl = convertUrl;
        }

        public async Task<IEnumerable<CurrencyDescriptor>> GetCurrencies()
        {
            using var response = await GetValidServiceResponse(_serviceCurrencyUrl)
                                        .ConfigureAwait(false);

            using var doc = await LoadJsonDocument(response).ConfigureAwait(false);

            return ParseCurrencies(doc);
        }

        public async Task<CurrencyConversion> GetConversionRate(
            string convertFromId,
            string convertToId)
        {
            string conversionId = $"{convertFromId}_{convertToId}";
            string fullUrl = $"{_serviceConvertUrl}&q={conversionId}";

            using var response = await GetValidServiceResponse(fullUrl)
                                       .ConfigureAwait(false);
            using var doc = await LoadJsonDocument(response)
                                  .ConfigureAwait(false);

            return ParseCurrencyConversionRate(
                doc, convertFromId, convertToId, conversionId);
        }

        private async Task<HttpResponseMessage> GetValidServiceResponse(string url)
        {
            var response = await _httpClient.GetAsync(url)
                                            .ConfigureAwait(false);
            if (!response.IsSuccessStatusCode)
            {
                throw new HttpRequestException(
                    $"Response status code does not indicate success: {response.StatusCode}.");
            }

            return response;
        }

        private async Task<JsonDocument> LoadJsonDocument(HttpResponseMessage response)
        {
            var json = await response.Content.ReadAsStreamAsync()
                                             .ConfigureAwait(false);
            return JsonDocument.Parse(json);
        }

        private IEnumerable<CurrencyDescriptor> ParseCurrencies(JsonDocument doc)
        {
            var results = new List<CurrencyDescriptor>();

            var element = doc.RootElement.GetProperty("results");
            foreach(var item in element.EnumerateObject())
            {
                var currency = ParseCurrencyDescriptor(item.Value);
                results.Add(currency);
            }

            return results.OrderBy(p => p.CurrencyName);
        }

        private CurrencyDescriptor ParseCurrencyDescriptor(JsonElement descriptorElement)
        {
            var id = descriptorElement.GetProperty("id").GetString();
            var name = descriptorElement.GetProperty("currencyName").GetString();
            var symbol = string.Empty;

            if (descriptorElement.TryGetProperty(
                "currencySymbol", out JsonElement symbolElement))
            {
                symbol = symbolElement.GetString();
            }

            return new CurrencyDescriptor
            {
                Id = id,
                CurrencyName = name,
                CurrencySymbol = symbol
            };
        }

        private CurrencyConversion ParseCurrencyConversionRate(
            JsonDocument doc, string convertFromId, string convertToId, string nodeName)
        {
            var element = doc.RootElement.GetProperty(nodeName);
            var val = element.GetProperty("val").GetDecimal();

            return new CurrencyConversion(convertFromId, convertToId, val); ;
        }
    }
}
  1. We define two base service URLs for getting the currency list and getting a conversion rate between two currencies. Note that both URLs use the apiKey we registered with the service. If you wish to use this service in your own application, you will need to register for your own api key.
  2. We have a constructor that takes the take the HttpClient to use for requests. This HttpClient is provided by the Blazor application via its dependency injection container.
  3. The GetCurrencies method (lines #32-40) retrieves the list of CurrencyDescriptors from the service. It gets an HTTP response from the service, loads the message payload into a JsonDocument, and then parses the currency list and converts it to model objects.
  4. The GetConversionRate method (lines #42-56) retrieves the conversion rate for a currency pair. This method takes the currencies’ Ids as parameters. It calls the appropriate endpoint with the currency pair to retrieve an appropriate response, loads the message payload into a JsonDocument, and then parses the currency rate message.
  5. The GetValidServiceResponse method (lines #58-69) performs a GET request on the specified URL. If the response is not a success status, then it throws an HttpRequestException). Exceptions has handled in our view model.
  6. The LoadJsonDocument method (lines #71-76) reads the JSON text from the HttpResponseMessage.Content property. Then creates an instance of JsonDocument that represents the loaded JSON.
  7. The ParseCurrencies method (lines #78-90) knows how to interpret this message’s JSON structure. It loops through all of the returned results nodes, parses each individual CurrencyDescriptor, and adds it to our list. At the end, we return that list.
  8. The ParseCurrencyDescriptor method (lines #92-110) converts a JsonElement into the expected properties for CurrencyDescriptor. If any conversion errors occur, this method will throw an exception that is also handled upstream by the view model. It loads a CurrencySymbol, if one is provided in the JSON, but it is not required, and some currencies do not provide one. Finally, it creates and returns a CurrencyDescriptor object with the converted data.
  9. The ParseCurrencyConversionRate method (lines #112-119) reads the conversion rate from the JsonDocument and returns an new instance of the CurrencyConversion model class.

This code is pretty clean and general purpose. Only the Parse* methods needs to change if the message format changes from version to version.

This class also encapsulates all of the web service interactions, so the rest of our application does not need to know how the currency data is retrieved. It could be from this web service, a different service with similar data that we could transform, from a JSON data file, or hard-coded test data for our unit tests.

CurrencyConverter View Model

Let’s create the CurrencyConverterViewModel class in the Blazor.AppIdeas.Converters project and ViewModels folder. This class is very similar to the view models in our other converter apps, so we will only review new concepts.

using Blazor.AppIdeas.Converters.Models;
using Blazor.AppIdeas.Converters.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Blazor.AppIdeas.Converters.ViewModels
{
    public class CurrencyConverterViewModel
    {
        private const string _defaultConvertFrom = "USD";
        private const string _defaultConvertTo = "EUR";
        private readonly ICurrencyServiceClient _serviceClient;

        public CurrencyConverterViewModel(ICurrencyServiceClient client)
        {
            _serviceClient = client ?? throw new ArgumentNullException(nameof(client));
        }

        public decimal ConvertFromValue { get; set; }

        public string ConvertFromCurrencyId { get; set; }

        public string ConvertFromCurrencySymbol =>
            FindCurrencySymbol(ConvertFromCurrencyId);

        public decimal ConvertToValue { get; private set; }
        
        public string ConvertToCurrencyId { get; set; }

        public string ConvertToCurrencySymbol =>
            FindCurrencySymbol(ConvertToCurrencyId);

        public IEnumerable<CurrencyDescriptor> Currencies { get; private set; } =
            new List<CurrencyDescriptor>();

        public decimal? ConversionRate { get; private set; }

        public string ErrorMessage { get; private set; }

        public bool HasError => !string.IsNullOrEmpty(ErrorMessage);

        public async Task Initialize()
        {
            try
            {
                Currencies = await _serviceClient.GetCurrencies()
                                                 .ConfigureAwait(false);
                ConvertFromCurrencyId = _defaultConvertFrom;
                ConvertToCurrencyId = _defaultConvertTo;
            }
            catch
            {
                ErrorMessage = "Cannot load currency list from service.";
            }
        }

        public async Task Convert()
        {
            try
            {
                ErrorMessage = null;
                if (ConvertFromValue < 0)
                    throw new ArgumentOutOfRangeException(nameof(ConvertFromValue));

                var conversionRate = await _serviceClient.GetConversionRate(
                                                            ConvertFromCurrencyId,
                                                            ConvertToCurrencyId)
                                                         .ConfigureAwait(false);

                ConversionRate = conversionRate.Rate;
                ConvertToValue = Math.Round(ConvertFromValue * ConversionRate.Value, 2);
            }
            catch (Exception ex)
            {
                ErrorMessage = $"Unable to convert between currencies. {ex.Message}.";
            }
        }

        public async Task SwapCurrencies()
        {
            var temp = ConvertFromCurrencyId;
            ConvertFromCurrencyId = ConvertToCurrencyId;
            ConvertToCurrencyId = temp;

            await Convert().ConfigureAwait(false);
        }

        private string FindCurrencySymbol(string currencyId)
        {
            if (string.IsNullOrEmpty(currencyId))
            {
                return string.Empty;
            }

            var result = Currencies.First(p => p.Id == currencyId)
                                   .CurrencySymbol;
            return result ?? string.Empty;
        }
    }
}

This view model class has a constructor (lines #16-19) that accepts an ICurrencyServiceClient object. This client object is what the view model uses to retrieve currency information. This object will be provided by the dependency injection container when this class is created.

The Initialize method (lines #44-57) is called by the Page during it’s initialization phase. It is also an async method. And, it is a good opportunity to load initial data, like the supported currency list. We load those into the Currencies property, and we set the starting, default ConvertFromCurrencyId and ConvertToCurrencyId properties (USD and EUR). This method handles any thrown exceptions, by setting the ErrorMessage property with an appropriate message.

The Convert method (lines #59-79) is very similar to the previous number conversion apps, but it gets the conversion rate from the service based on the user’s selected ConvertFromCurrencyId and ConvertToCurrencyId values. Then, it does the mathematical conversion. If any exceptions were thrown in this process, we set another ErrorMessage.

Finally, the Swap method (lines #81-88) swaps the ConvertFromCurrencyId and ConvertToCurrencyId values and then calls Convert.

CurrencyConverter Page

We will create a page with a numeric input control for the amount to convert, a dropdown list of the source currency, and then a dropdown list of the target result’s currency. Finally, we have some display labels for the conversion rate and the converted amount. This page will be very similar to the number converter pages.

Let’s create the CurrencyConverter page in the Blazor.AppIdeas.Converters project and Pages folder.

@page "/currency-convert"
@inject CurrencyConverterViewModel vm

<div class="col-12 mt-3 pb-3 container">
    <h3 class="mt-3">Currency Converter</h3>
    <hr />
    <EditForm class="form-row" Model=@vm>
        <div class="form-group input-group col-lg-5 col-md-12">
            <div class="input-group-prepend">
                <span class="input-group-text">@vm.ConvertFromCurrencySymbol</span>
            </div>
            <InputNumber id="convert-from-value" class="form-control"
                            min="0.00" step="0.01" placeholder="Enter amount"
                            @bind-Value=vm.ConvertFromValue />
            <InputSelect id="convert-from-currency-id" class="col-12"
                         style="margin-top: 1px"
                         @bind-Value=vm.ConvertFromCurrencyId>
                @foreach(var currency in vm.Currencies)
                {
                 <option value="@currency.Id">
                    @currency.CurrencyName [@currency.Id]
                 </option>
                }
            </InputSelect>
        </div>
        <div class="form-group col-lg-2 col-md-12 text-center">
            <button id="btn-swap" style="width: 48px"
                    class="btn btn-outline-primary col-lg-12 col-sm-6"
                    @onclick="vm.SwapCurrencies">
                <span class="oi oi-loop-circular" aria-hidden="true" />
            </button>
            @if (vm.ConversionRate is not null)
            {
             <label class="col-lg-12 col-sm-6 mt-2">
                <strong>Rate:</strong> @vm.ConversionRate
             </label>
            }
        </div>
        <div class="form-group input-group col-lg-5 col-md-12">
            <div class="input-group-prepend">
                <span class="input-group-text">@vm.ConvertToCurrencySymbol</span>
            </div>
            <label id="convert-to-value" class="form-control mb-0">
                @vm.ConvertToValue
            </label>
            <InputSelect id="convert-to-currency-id" class="col-12"
                         style="margin-top: 1px"
                         @bind-Value=vm.ConvertToCurrencyId>
                @foreach(var currency in vm.Currencies)
                {
                 <option value="@currency.Id">
                    @currency.CurrencyName [@currency.Id]
                 </option>
                }
            </InputSelect>
        </div>
        @if (vm.HasError)
        {
            <div class="alert alert-danger col-12 ml-1">
                <strong>Error:</strong> @vm.ErrorMessage
            </div>
        }
    </EditForm>

    <div class="text-center">
        <input type="button" id="btn-convert" class="btn btn-outline-primary"
                value="Convert" @onclick="@vm.Convert" />
    </div>
</div>

@code {
    protected override async Task OnInitializedAsync()
    {
        await vm.Initialize().ConfigureAwait(false);
    }
}

Most of the controls and layout is standard, so let’s just look at some items of interests:

  • In line #2, we inject the CurrencyConverterViewModel which also has a dependency on ICurrencyServiceClient. Both will be registered in the dependency injection container. And when the view model is created the CurrencyServiceClient will also be created and passed to the view model’s constructor.
  • The input-group-prepend Bootstrap CSS shows how a label can be attached to an input control.
  • We use the InputNumber Blazor component to bind to a numeric control and a decimal-typed value.
  • Lines #18-23 enumerate the Currencies property and creates an option for each current. The value is the Id and the display text is the CurrencyName and Id.
  • In the @code section, we override the Page.OnInitializedAsync method (lines #72-75). This method is called during the page initialization process. It is an opportunity to do one-time custom initialization. In this method, we call the CurrencyConverterViewModel.Initialize method.

Additional Changes

As with other converter projects, we need to add a link to the NavMenu and update the dependency injection container.

@attribute [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">AppIdeas - Converters</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="bin2dec">
                <span class="oi oi-arrow-bottom" aria-hidden="true"></span> Bin2Dec
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="rom2dec">
                <span class="oi oi-move" aria-hidden="true"></span> Roman2Dec
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="number-converter">
                <span class="oi oi-loop-circular" aria-hidden="true"></span> Ultra Number Converter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="dol2cent">
                <span class="oi oi-dollar" aria-hidden="true"></span> Dollar2Cents
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="currency-convert">
                <span class="oi oi-euro" aria-hidden="true"></span> Currency Converter
            </NavLink>
        </li>
    </ul>
</div>

@code {
    private bool collapseNavMenu = true;

    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}
using Blazor.AppIdeas.Converters.Services;
using Blazor.AppIdeas.Converters.ViewModels;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Threading.Tasks;

namespace Blazor.AppIdeas.Converters
{
    [ExcludeFromCodeCoverage]
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            var httpClient = new HttpClient
            {
                BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
            };
            builder.Services.AddScoped(sp => httpClient);

            // add services and view models to DI container.
            builder.Services.AddSingleton<ICurrencyServiceClient>(
                sp => new CurrencyServiceClient(httpClient));

            builder.Services.AddTransient<RomanDecimalConverter>();
            builder.Services.AddTransient<NumberConverterViewModel>();
            builder.Services.AddTransient<DollarCentsConverterViewModel>();
            builder.Services.AddTransient<CurrencyConverterViewModel>();

            await builder.Build().RunAsync();
        }
    }
}
  • We refactored the HttpClient creation and registration (lines #20-24), so that we can use it for our service creation too.
  • Then, we register the ICurrencyServiceClient type as a singleton for the lifetime of the application (lines #27-28). We provide a factory method that knows how to construct the service, that is called when this type is requested for the first time.
  • Finally, we register the CurrencyConverterViewModel class as well (line #33).

With all of this code completed, we can build and run the project. If we navigate to the Currency Converter, we can try some conversions.

Fig 1 – Currency Converter Page

With the Bootstrap CSS elements that we have been using, our page lays out well at different sizes. If we shrink the width of the browser, we can see what this converter would look like on a mobile device.

Fig 2 – Currency Converter Vertical

We also have a full suite of tests for this converter with tests for the service client and mocking the ICurrencyServiceClient in our view model and page tests. These tests are part of the commit for this project. We can build and run all of the tests to validate our application works as expected.

In conclusion, we created a new currency converter app. In this app, we learned:

  • How to make calls to public web services from our Blazor app.
  • To convert JSON message payloads into model classes (using System.Text.JSON).
  • To add the ICurrencyServiceClient layer to encapsulate the service interactions and isolate that from our MVVM classes.
  • To use the page initialization methods to initialize the currency data in our view model.
  • How to chain dependency injection to create our view model, which then creates or retrieves the ICurrencyServiceClient implementation.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s