The inevitability of most Blazor applications is the use of HttpClient
to reach out to API endpoints. Many times, these endpoints are contained within the solution, but they may be external as well. Without much effort, this necessity quickly turns into a rats nest of initialization code in razor code with hardcoded strings for each endpoint. The pattern I will describe in this post is designed to make this a simpler experience.
For the purposes of this post, I’m going to use the Blazor WebAssembly template with ASP.NET Core hosting to keep client and server separate – mostly to demonstrate the capabilities in a cross assembly fashion.
Hot Tip
Just because the default templates define a “Shared” library and namespace, it can often make sense to trim out the “Shared” portion from the default namespace to simplify razor using statements.
The first thing we need to do is define the functionality our service will support. This should match, at least partially, the methods defined by your API controllers. We’ll use the built in Weather Forecast as our example.
public interface IWeatherService { Task<IEnumerable<WeatherForecast>> Get(); }
Now that we have an interface defined, we will add it to the WeatherForecastController
class definition. Other than updating the method to be async and return a Task, no other changes should be necessary since the interface matches the definition of the controller.
Now, before we get to the magic – I’m going to define a helper interface. While this is not necessary to everyone’s use case, I found it useful to stretch the use of this pattern across a Blazor site and a WPF desktop application with more complex serialization scenarios.
The IJsonOptionsProvider
service defines a method for retrieving an instance of JsonOptions that allow for customization of the serialization process, including custom converters for the (currently) nefarious TimeSpan
object.
public interface IJsonOptionsProvider { JsonSerializerOptions Options { get; } }
The real magic comes in the HttpClientApiBase abstraction, which provides a common method for interacting with HttpClient configurations. While the API Controller implements the server side of our custom Weather service, this class will provide the Blazor Client access to the same interface.
public abstract class HttpClientApiBase { private readonly IJsonOptionsProvider _jsonOptionsProvider; public HttpClientApiBase(IHttpClientFactory httpClientFactory, INavigationProvider navigationProvider, IJsonOptionsProvider jsonOptionsProvider) { _jsonOptionsProvider = jsonOptionsProvider; Navigation = navigationProvider; ClientFactory = httpClientFactory; } protected async Task<TResult> DeleteAsync<TResult>(string path, string clientName = "api") { var client = ClientFactory.CreateClient(clientName); var result = await client.DeleteAsync( Navigation.Format(path) ); return await result.Content.ReadFromJsonAsync<TResult>(_jsonOptionsProvider.Options); } protected async Task<TResult> PostAsync<TModel, TResult>(string path, TModel model, string clientName = "api") { var client = ClientFactory.CreateClient(clientName); var result = await client.PostAsJsonAsync<TModel>( Navigation.Format(path), model, _jsonOptionsProvider.Options ); return await result.Content.ReadFromJsonAsync<TResult>(_jsonOptionsProvider.Options); } protected async Task<TResult> GetAsync<TResult>(string path, string clientName = "api") { var client = ClientFactory.CreateClient(clientName); var result = await client.GetFromJsonAsync<TResult>( Navigation.Format(path), _jsonOptionsProvider.Options ); return result; } protected INavigationProvider Navigation { get; private set; } protected IHttpClientFactory ClientFactory { get; private set; } }
Please note there is a general lack of optimization, specifically in the lack of reuse of an HttpClient. This is for sake of simplicity over outright performance.
To expose the functionality to the Client project, define an HttpClientWeatherService class that implements the above abstraction and the IWeatherService interface. By using the internal methods, we can interact with an API endpoint via HttpClient and return an object. This allows initialization code to be centralized, following the DRY (dont repeat yourself) principle.
public sealed class HttpClientWeatherService : HttpClientApiBase, IWeatherService { public HttpClientWeatherService(IHttpClientFactory httpClientFactory, IJsonOptionsProvider jsonOptionsProvider) : base(httpClientFactory, jsonOptionsProvider) { } public async Task<IEnumerable<WeatherForecast>> Get() { return await GetAsync<IEnumerable<WeatherForecast>>("/api/weatherforecast"); } }
Lastly (for setup), we need to inject the various services we need, and configure the named HttpClient. Change the Client’s Program.Main method as follows.
public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("app"); // builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddHttpClient("api", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)); builder.Services.AddScoped<IJsonOptionsProvider, StaticJsonOptionsProvider>(); builder.Services.AddScoped<IWeatherService, HttpClientWeatherService>(); await builder.Build().RunAsync(); }
If everything is wired up correctly, we should now be able to use this IWeatherService inside the razor pages! And instead of interacting directly with IHttpClientFactory or injected HttpClient instances, we go through a well-typed service class.
forecasts = await WeatherService.Get();
Hopefully this demonstrates a useful pattern for injecting well typed services that interact with API endpoints via the internal HttpClient classes while hiding the details of those interactions. We improve front-end development by simplifying their API while centralizing the initialization and configuration code.
And as always, the code is available for free on github at https://github.com/jsedlak/samples.injectedservices