You're building ASP .NET web APIs that need to communicate with external APIs.
The UserRegister API needs to insert a new user into the database, but also talk to three different services:
- FileStoreService - uploads the user's profile photo
- PDFService - generates a new document for the user to sign
- EmailService - sends PDF as an email attachment.
You need to register an HTTP client for each external service you want to connect to from your service. A common approach is to use the HTTPClientFactory and the named clients as follows:
Add clients to the appsettings.json
{
"FileStorageServiceUri": "https//file-storage-service:1234",
"PDFServiceURI": "https//pdf-service:4567",
"EmailServiceUri": "https//email-service:8883"
}
Register each (named) HTTP client in Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("FileStorage", c =>
{
c.BaseAddress = new Uri(builder.Configuration["FileStorageUri"]);
c.DefaultRequestHeaders.Add("Accept", "application/json");
});
builder.Services.AddHttpClient("PDF", c =>
{
c.BaseAddress = new Uri(builder.Configuration["PDFServiceURI"]);
c.DefaultRequestHeaders.Add("Accept", "application/json");
});
builder.Services.AddHttpClient("Email", c =>
{
c.BaseAddress = new Uri(builder.Configuration["EmailServiceUri"]);
c.DefaultRequestHeaders.Add("Accept", "application/json");
});
var app = builder.Build();
// ...
app.Run();
In the user service, you'd invoke each service using the HTTP Client Factory.
public class UserService(IHttpClientFactory httpFactory) : IUserService
{
private readonly IHttpClientFactory _httpClientFactory = httpFactory;
public async Task<User> RegisterUser(UserDTO newUser)
{
//Invoke the File storage service
var fileStorageClient = _httpClientFactory.CreateClient("FileStorage");
HttpResponseMessage fsResponse = await fileStorageClient.PostAsJsonAsync("/upload", newUser.ProfilePhoto);
if (!fsResponse.IsSuccessStatusCode)
{
// handle error
}
// handle response
var base64Encoded = await fsResponse.Content.ReadAsStringAsync();
// Save user to DB
// Generate PDF
// Send Email
}
}
If you continue down this path of
- instantiating an HTTP client for each external service call and
- validating the request before sending and handling the response after for each client
- as well as writing the rest of the business logic in the same method
The RegisterUser method will become unreadable very soon.
The solution - Separate class per HTTP client
Move the business logic of each external service into its own service class:
- FileStorageService
- PDFService
- EmailService
public interface IFileStorageService
{
Task<string> UploadProfilePhoto(UserDTO newUser);
}
Create an HTTP client that talks to an external service using the factory:
public class FileStorageService(IHttpClientFactory httpFactory) : IFileStorageService
{
private readonly IHttpClientFactory _httpClientFactory = httpFactory;
public async Task<string> UploadProfilePhoto(UserDTO newUser)
{
// Invoke the File storage service
var fileStorageClient = _httpClientFactory.CreateClient("FileStorage");
HttpResponseMessage fsResponse = await fileStorageClient.PostAsJsonAsync("/upload", newUser.ProfilePhoto);
if (!fsResponse.IsSuccessStatusCode)
{
// handle error
}
// handle response
return await fsResponse.Content.ReadAsStringAsync();
}
}
Also, inject the file storage service as Scoped or Transient:
builder.Services.AddScoped<IFileStorageService, FileStorageService>();
// ...
var app = builder.Build();
Now, the FileStorageService handles all calls to the File Storage microservice. All UserService has to do is inject the interface and invoke the upload method when needed:
public class UserService(IFileStorageService fileStorageService): IUserService
{
private readonly IFileStorageService _fileStorageService = fileStorageService;
public async Task<User> RegisterUser(UserDTO newUser)
{
var profileImage = await _fileStorageService.UploadProfilePhoto(newUser);
// Save user to DB
// Generate PDF
// Send Eemail
}
}
The same applies to calls to the remaining external services.
The Problem
Even though you did write all the FileService-related logic into its own class, there is nothing stopping anyone from injecting an HttpClientFactory in a different class and creating another instance of the FilesStorageService:
public class PetService(IHttpClientFactory httpFactory): IPetService
{
private readonly IHttpClientFactory _httpClientFactory = httpFactory;
public async Task<Pet> CreatePet(Pet newPet)
{
var fileStorageClient = _httpClientFactory.CreateClient("FileStorage");
// ...
}
}
In other words, you should prohibit access to the FileStorage HTTP Client to any class that does not use the designated service (FileStorageService).
How to do that?
Typed HTTP Clients
Rather than injecting the HTTP client and the FileStorage service separately, you'll combine the two into one injection:
// transient injection
builder.Services.AddHttpClient<IFileStorageService, FileStorageService>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["FileStorageServiceUri"]);
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
// delete the previous HTTP Client and Services.AddScoped<>() injection
This tells the project that only the FileStorage service has access to the FileStorage external service (via HTTP client).
It also means that the FileStorage service will no longer use the HttpClientFactory to create new clients. Instead, the service contains the HttpClient with a base address pointing to the FileStorage external microservice.
public class FileStorageService(HttpClient fileStorageClient) : IFileStorageService
{
public async Task<string> UploadProfilePhoto(UserDTO newUser)
{
// Invoke the File storage service
HttpResponseMessage fsResponse = await fileStorageClient.PostAsJsonAsync("/upload", newUser.ProfilePhoto);
// ...
}
}
The UserService can continue calling the FileStorage Service using the interface (as before):
public class UserService(IFileStorageService fileStorageService): IUserService
{
private readonly IFileStorageService _fileStorageService = fileStorageService;
public async Task<User> RegisterUser(UserDTO newUser)
{
var profileImage = await _fileStorageService.UploadProfilePhoto(newUser);
// ...
}
But the UserService can't, nor can any other class, inject the HTTP Client Factory that talks to the FileStorage microservice. There simply isn't a name to provide:
var client = _httpClientFactory.CreateClient("There is no name");
Note: This solution isn't well-suited to singleton services.
Summary
This article explains how to use the dependency inversion principle to abstract business (domain) HTTP calls within services that handle them, preventing other services from making the same HTTP calls on their behalf.
Top comments (0)