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 ofCurrencyDescriptor
model objects. These would represent the currency types supported by this service. - The
GetConversionRate
method takes the source and destination currency ids and returns theCurrencyConversion
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); ;
}
}
}
- 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.
- We have a constructor that takes the take the
HttpClient
to use for requests. ThisHttpClient
is provided by the Blazor application via its dependency injection container. - The
GetCurrencies
method (lines #32-40) retrieves the list ofCurrencyDescriptors
from the service. It gets an HTTP response from the service, loads the message payload into aJsonDocument
, and then parses the currency list and converts it to model objects. - 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 aJsonDocument
, and then parses the currency rate message. - 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 anHttpRequestException
). Exceptions has handled in our view model. - The
LoadJsonDocument
method (lines #71-76) reads the JSON text from theHttpResponseMessage.Content
property. Then creates an instance ofJsonDocument
that represents the loaded JSON. - 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 individualCurrencyDescriptor
, and adds it to our list. At the end, we return that list. - The
ParseCurrencyDescriptor
method (lines #92-110) converts aJsonElement
into the expected properties forCurrencyDescriptor
. If any conversion errors occur, this method will throw an exception that is also handled upstream by the view model. It loads aCurrencySymbol
, if one is provided in the JSON, but it is not required, and some currencies do not provide one. Finally, it creates and returns aCurrencyDescriptor
object with the converted data. - The
ParseCurrencyConversionRate
method (lines #112-119) reads the conversion rate from theJsonDocument
and returns an new instance of theCurrencyConversion
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 onICurrencyServiceClient
. Both will be registered in the dependency injection container. And when the view model is created theCurrencyServiceClient
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 theId
and the display text is theCurrencyName
andId
. - In the
@code
section, we override thePage.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 theCurrencyConverterViewModel.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.

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.

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.