DEV Community

Cover image for Build Domain-driven HTTP services
Mirza Leka
Mirza Leka

Posted on

Build Domain-driven HTTP services

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"
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
Enter fullscreen mode Exit fullscreen mode

Also, inject the file storage service as Scoped or Transient:

builder.Services.AddScoped<IFileStorageService, FileStorageService>();

// ...

var app = builder.Build();
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
Enter fullscreen mode Exit fullscreen mode

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");
            // ...

        }
    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);

            // ...
        }
    }
Enter fullscreen mode Exit fullscreen mode

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);

            // ...
        }
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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)