Blazor, HttpClientFactory, and Web API

BLAZOR SERVER-SIDE USE HTTPCLIENTFACTORY TO CONSUME EXTERNAL API

by

In this tutorial, you will learn how to create a server-side Blazor application that interacts with an external web API using HttpClientFactory. Later in the series, you will add IdentityServer4 authentication to protect the API and authorize the client web app.

In Part 1, you will create a public Web API, and you will learn the right way to interact with it from a server-side Blazor app. In the next tutorial, you will protect the API using IdentityServer4 and learn how to authorize your Blazor app using an access token.

In this Blazor tutorial series

Though this tutorial is written for server-side Blazor applications, the techniques can also be used in other ASP.NET web apps, including both MVC and Razor Pages projects.

Create the Shared Models Project

Start by creating a shared library that will contain the models to be used in the solution. The models contained in the shared library will be referenced by both the API and the Blazor web application frontend, so this is a good place to start!

Launch Visual Studio and create a New Project. Select Class Library (.NET Standard). A .NET Standard class library can be added as a reference to all .NET Core web applications (MVC, Razor Pages, and Blazor), and it can also be included as a reference in a Xamarin project if you ever decide to create a mobile frontend.

In this series, you will create a contact management application. For Solution Name, type BlazorContacts. Enter BlazorContacts.Shared for the Project Name, and click Create to scaffold the shared library from template.

In the Solution Explorer pane, right-click the BlazorContacts.Shared project and select Add > New Folder. Name the folder Models. This directory will contain all the shared models you will need in your application

For this application, you will need a model that contains information about individual contacts. Right-click the Models folder, and select Add > Class. Name the class Contact.cs. This will generate a file that should look like the following:

using System;
using System.Collections.Generic;
using System.Text;

namespace BlazorContacts.Shared.Models
{
    class Contact
    {
    }
}

The Contact model should be public, so you can access it from outside the class. It should also include a unique identifier for the contact, and, for this example, the contact’s name and phone number. You may choose to split the Name property into two properties, one for FirstName and one for LastName, to provide for more robust sorting and filtering in your end product. You could also add more fields, such as Address, EmailAddress, and Location.

namespace BlazorContacts.Shared.Models
{
    public class Contact
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public string PhoneNumber { get; set; }
    }
}

There is a useful Nuget package for annotating data models that I recommend. Using it will make data validation much easier on both the backend database as well as the frontend UI. It can be used to define required fields, length constraints, valid characters, etc. Add the package to your project.

Install-Package System.ComponentModel.Annotations

Then include the package in your class library. By also including a JSON serialization library, you can ensure the public properties in your model are linked to the correct JSON property produced by the API.

using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;

I am using the new System.Text.Json namespace instead of NewtonSoft. With this package, your annotated Models class might look like the following:

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;

namespace BlazorContacts.Shared.Models
{
    public class Contact
    {
        [Key]
        [JsonPropertyName("id")]
        public long Id { get; set; }

        [Required]
        [JsonPropertyName("name")]
        public string Name { get; set; }

        [Required]
        [DisplayName("Phone Number")]
        [JsonPropertyName("phonenumber")]
        public string PhoneNumber { get; set; }
    }
}

The [Key] attribute can be used when attaching the model to a database. It is not actually necessary in this case, because a property with name Id will automatically be used as the key in a database entry. The [Required] attribute denotes a required field, and DisplayName allows you to denote a human readable name for a field. The DisplayName will be shown in the case of error messages, for example.

Create the Web API

Now, it is time to create the Web API which will be used to add, delete, and fetch contacts. With the BlazorContacts solution open, add a New Project , and select ASP.NET Core Web Application. Name the project BlazorContacts.API , and click Create. On the next page, select the API project template.

Leave the Authentication setting as No Authentication. Later, you will configure IdentityServer4 to grant API access to your Blazor frontend. Click Create , and wait for the API project template to scaffold.

In the Solution Explorer pane of your newly created API project, right click the BlazorContacts.API project and select Add > Reference. In the Reference Manager, add BlazorContacts.Shared as a reference for your API project. This will allow you to reference the Contact model you just created.

Next, right-click the Controllers directory of the API project and select Add > Controller. In the Add New Scaffolded Item dialog, choose API Controller – Empty and click Add. In the next dialog, name the controller ContactsController. This will scaffold a blank API controller class called ContactsController.cs that looks like the following:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace BlazorSecure.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ContactsController : ControllerBase
    {
    }
}

Add a using directive to include the BlazorContacts.Shared.Models namespace.

using BlazorContacts.Shared.Models;

For demonstration purposes, this API will simply store the contacts in memory. In production applications, you could use a database. Declare a collection of contacts, and add a method to populate the collection.

private static readonly List<Contact> contacts = GenerateContacts(5);
private static List<Contact> GenerateContacts(int number)
{
    return Enumerable.Range(1, number).Select(index => new Contact
    {
        Id = index,
        Name = $"First{index} Last{index}",
        PhoneNumber = $"+1 555 987{index}",
    }).ToList();
}

The GenerateContacts() method will generate and return a list of a given number of unique contacts. You could also generate contacts one by one and add them to the contacts variable. Again, this is all just sample data stored in memory, so do whatever you’d like.

contacts.Add(new Contact { Id = 1, Name="First1 Last1", PhoneNumber="+1 555 123 9871" });

Next, add public methods for interacting with the API. Below are sample methods.

// GET: api/contacts
[HttpGet]
public ActionResult<List<Contact>> GetAllContacts()
{
    return contacts;
}

// GET: api/contacts/5
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Contact> GetContactById(int id)
{
    var contact = contacts.FirstOrDefault((p) => p.Id == id);
    if (contact == null)
        return NotFound();
    return contact;
}

// POST: api/contacts
[HttpPost]
public void AddContact([FromBody] Contact contact)
{
    contacts.Add(contact);
}

// PUT: api/contacts/5
[HttpPut("{id}")]
public void EditContact(int id, [FromBody] Contact contact)
{
    int index = contacts.FindIndex((p) => p.Id == id);
    if(index != -1)
        contacts[index] = contact;

}

// DELETE: api/contacts/5
[HttpDelete("{id}")]
public void Delete(int id)
{
    int index = contacts.FindIndex((p) => p.Id == id);
    if (index != -1)
        contacts.RemoveAt(index);
}

There are two GET methods, one for fetching all contacts and one for getting individual contacts by ID. Next, there is a POST method for adding a new contact. The PUT method can be used to update an existing contact, and the DELETE method will delete a contact by ID.

One final note regarding the BlazorContacts.API project: I am using the following launch settings (Properties > launchSettings.json) to start the project on http://localhost:5001. This address will be used when configuring the Identity Server authentication and when setting the base api address for the web frontend.

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:5001",
      "sslPort": 0
    }
  },
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchUrl": "api/contacts",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "BlazorContacts.API": {
      "commandName": "Project",
      "launchUrl": "api/contacts",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:5001"
    }
  }
}

Create the Blazor Web App

Now it is time to create the web interface to interact with the API. Create a New Project within the BlazorContacts solution. Select Blazor App and name the project BlazorContacts.Web. On the next page, select Blazor Server App.

First, from the new BlazorContacts.Web project, Add > Reference that points to the BlazorContacts.Shared project, just like you did when creating the API. Then add a new folder called Services to the BlazorContacts.Web project. Right-click the new folder and Add > New Class called ApiService.cs.

The ApiService service will use the IHttpClientFactory interface, which is the best way to use HttpClient in a server-side Blazor application. HttpClientFactory ensures that the sockets associated with each HttpClient instance are shared, thus preventing the issue of socket exhaustion.

using BlazorContacts.Shared.Models;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace BlazorContacts.Web.Services
{

    public class ApiService
    {
        public HttpClient _httpClient;

        public ApiService(HttpClient client)
        {
            _httpClient = client;
        }

        public async Task<List<Contact>> GetContactsAsync()
        {
            var response = await _httpClient.GetAsync("api/contacts");
            response.EnsureSuccessStatusCode();

            using var responseContent = await response.Content.ReadAsStreamAsync();
            return await JsonSerializer.DeserializeAsync<List<Contact>>(responseContent);
        }

        public async Task<Contact> GetContactByIdAsync(int id)
        {
            var response = await _httpClient.GetAsync($"api/contacts/{id}");
            response.EnsureSuccessStatusCode();

            using var responseContent = await response.Content.ReadAsStreamAsync();
            return await JsonSerializer.DeserializeAsync<Contact>(responseContent);
        }
    }
}

In this example, I have written two methods for the ApiService class. One will return a List<Contact> and the other will return an individual Contact by its unique ID property. You could also just use a single method that returns a string and perform the deserialization in your page controller, instead.

return await response.Content.ReadAsStringAsync();

Now that the service is created, you must register the HttpClientFactory interface. Still in BlazorContacts.Web , open Startup.cs and locate the public void ConfigureServices(IServiceCollection services) method and register the ApiService typed client.

services.AddHttpClient<Services.ApiService>(client =>
{
    client.BaseAddress = new Uri("http://localhost:5001");
});

Go ahead and assign BaseAddress to the address where your web API is located, as shown above. The path argument in the the GetApiAsync() method of the ApiService class will append to this base address. Passing “api/contacts” to the method, for example, will fetch the result from http://localhost:5001/api/contacts which is configured in the API’s ContactsController.cs file to return a list of all contacts.

Create the Blazor Page

All that is left is to create Blazor pages that will interact with the API, through the ApiService. Create a new Razor component in the Pages folder called Contacts.Razor. To make this page appear at the /contacts route, add a page directive to the top of the page.

@page "/contacts"

Next, inject the HttpClientFactory interface you previously registered, and import the namespace of the shared contact model.

@inject Services.ApiService apiService
@using BlazorContacts.Shared.Models

You can use the service in your page code as follows. In this case, I am overriding the OnInitialized method of the Blazor component so I can use the List<Contact> in the web interface.

List<Contact> contacts;

protected override async Task OnInitializedAsync()
{
    contacts = await apiService.GetContactsAsync();
}

A complete Blazor page that relies on apiService.GetContactsAsync() may look like the following.

@page "/contacts"
@inject Services.ApiService apiService
@using BlazorContacts.Shared.Models


<h3>Contacts</h3>
@if (contacts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Phone Number</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var contact in contacts)
            {
                <tr>
                    <td>@contact.Id</td>
                    <td>@contact.Name</td>
                    <td>@contact.PhoneNumber</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    List<Contact> contacts;

    protected override async Task OnInitializedAsync()
    {
        contacts = await apiService.GetContactsAsync();
    }
}

To use the GetContactByIdAsync() method, simply pass the id value of the contact whose information you want to retrieve to the method.

Contact contact3 = await apiService.GetContactByIdAsync(3);

Putting it all together

To test your Blazor solution, you will need to launch the Web API and the Blazor App at the same time. To do this, you must configure multiple projects to startup when you launch the debugger.

Right click the BlazorContacts solution in the Solution Explorer. Select Set StartUp Projects. In the dialog that opens, choose Multiple startup projects and set the action for BlazorContacts.API and BlazorContacts.Web to Start.

Now, when you start debugging, the API will run on http://localhost:5001 and the web server will run on another port. I have configured mine to run on http://localhost:5002, for example. When you navigate to the /contacts page, the server-side Blazor frontend will fetch data from the external API using techniques that will properly dispose HttpClient and not result in socket exhaustion.

Source code for this project is available on GitHub. In the coming tutorials, we will build off this application by adding IdentityServer4 protection to the API.


Don't stop learning!

There is so much to discover about C#. That's why I am making my favorite tips and tricks available for free. Enter your email address below to become a better .NET developer.


Did you know?

Our beautiful, multi-column C# reference guides contain more than 150 tips and examples to make it even easier to write better code.

Get your cheat sheets