Web API – IHttpClientFactory

In this post, we will see how to use IHttpClientFactory, in order to invoke a Web API.
IHttpClientFactory is available since .NET Core 2.1 and it provides a central location for naming, configuring and consuming logical HttpClients in our application.

For this post, we will use the project created in the post: Web API – Gateway with Ocelot and, we will add a new Microservice called Api.Orders.
In Api.Orders we will call the other two Microservices (Api.Customers and Api.Books), in order to create a list of Orders.
We will create this new Microservices just to see how to call other Microservices with IHttpClientFactory so, for this reason, we will not create all the objects used in the other Microservices and we will not use the InMemory Database provider.

We start, adding in the appsettings file the urls of the two Microservices:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Services": {
    "Books": "http://localhost:52043",
    "Customers": "http://localhost:52043"
  }
}

In this case, the urls are the same because we have implemented a Gateway.



Then, we modify the method ConfigureServices in Startup file, in order to register the IHttpClientFactory into Api.Orders:

public void ConfigureServices(IServiceCollection services)
{
     services.AddHttpClient("BookService", config =>
     {
         config.BaseAddress = new Uri(Configuration["Services:Books"]);
     });
     services.AddHttpClient("CustomerService", config =>
     {
         config.BaseAddress = new Uri(Configuration["Services:Customers"]);
     });

     services.AddControllers();
}



Now, we will add four entities that we will use in the orders list:
[Order.cs]

using System;
using System.Collections.Generic;

namespace Api.Orders.Db
{
    public class Order
    {
        public int Id { get; set; }
        public string EmailCustomer { get; set; }
        public DateTime OrderDate { get; set; }
        public decimal Total { get; set; }
        public List<OrderItem> Books { get; set; }
    }
}


[OrderItem.cs]

namespace Api.Orders.Db
{
    public class OrderItem
    {
        public string BookTitle { get; set; }
        public int Quantity { get; set; }
        public decimal Price { get; set; }
    }
}


[Book.cs]

namespace Api.Orders.Core.Model
{
    public class Book
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public decimal Price { get; set; }
    }
}


[Customer.cs]

namespace Api.Orders.Core.Model
{
    public class Customer
    {
        public int Id { get; set; }
        public string UserName { get; set; }
        public string Email { get; set; }
    }
}



Then, we define two Classes called BooksService and CustomersService that we will use to call Api.Books and Api.Customers:

[IBooksService.cs]

using Api.Orders.Core.Model;
using System.Threading.Tasks;

namespace Api.Orders.Core.Interfaces
{
    public interface IBooksService
    {
        Task<(bool IsSuccess, Book ObjBook)> GetBook(int id);
    }
}


[BooksService.cs]

using Api.Orders.Core.Interfaces;
using Api.Orders.Core.Model;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace Api.Orders.Core.Providers
{
    public class BooksService : IBooksService
    {
        private readonly IHttpClientFactory httpClientFactory;
        public BooksService(IHttpClientFactory inputHttpClientFactory)
        {
            // Definition of httpclientfactory
            httpClientFactory = inputHttpClientFactory;
        }

        public async Task<(bool IsSuccess, Book ObjBook)> GetBook(int id)
        {
            try
            {
                // Definition of the call at Api.Books
                var microserviceBook = httpClientFactory.CreateClient("BookService");

                // Call the method GetBookAsync
                var responseMicroserviceBook = await microserviceBook.GetAsync($"api/books/{id}");

                // Verify of the response
                if (responseMicroserviceBook.IsSuccessStatusCode)
                {
                    // Api.Books output
                    var outputMicroserviceBook = await responseMicroserviceBook.Content.ReadAsByteArrayAsync();
                    var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
                    // Deserialization of the output
                    var outputBook = JsonSerializer.Deserialize<Book>(outputMicroserviceBook, options);
                    
                    return (true, outputBook);
                }
                return (false, null);
            }
            catch
            {
                return (false, null);
            }
        }
    }
}


[ICustomersService.cs]

using Api.Orders.Core.Model;
using System.Threading.Tasks;

namespace Api.Orders.Core.Interfaces
{
    public interface ICustomersService
    {
        Task<(bool IsSuccess, Customer ObjCustomer)> GetCustomer(int id);
    }
}


[CustomersService]

using Api.Orders.Core.Interfaces;
using Api.Orders.Core.Model;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace Api.Orders.Core.Providers
{
    public class CustomersService : ICustomersService
    {
        private readonly IHttpClientFactory httpClientFactory;
        public CustomersService(IHttpClientFactory inputHttpClientFactory)
        {
            // Definition of httpclientfactory
            httpClientFactory = inputHttpClientFactory;
        }

        public async Task<(bool IsSuccess, Customer ObjCustomer)> GetCustomer(int id)
        {
            try
            {
                // Definition of the call at Api.Customers
                var microserviceCustomer = httpClientFactory.CreateClient("CustomerService");

                // Call the method GetCustomerAsync
                var responseMicroserviceCustomer = await microserviceCustomer.GetAsync($"/api/customers/{id}");

                // Verify of the response
                if (responseMicroserviceCustomer.IsSuccessStatusCode)
                {
                    // Api.Customers output
                    var outputMicroserviceCustomer = await responseMicroserviceCustomer.Content.ReadAsByteArrayAsync();
                    var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
                    // Deserialization of the output
                    var outputCustomer = JsonSerializer.Deserialize<Customer>(outputMicroserviceCustomer, options);

                    return (true, outputCustomer);
                }
                return (false, null);
            }
            catch
            {
                return (false, null);
            }
        }
    }
}



Finally, we define a new Class called CoreOrder that we will use to create the orders list:

[ICoreOrder.cs]

using Api.Orders.Db;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Api.Orders.Core.Interfaces
{
    public interface ICoreOrder
    {
        Task<(bool IsSuccess, IEnumerable<Order> Orders, string ErrorMessage)> GetOrdersAsync();
    }
}


[CoreOrder.cs]

using Api.Orders.Core.Interfaces;
using Api.Orders.Db;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Api.Orders.Core.Providers
{
    public class CoreOrder : ICoreOrder
    {
        private readonly IBooksService booksService;
        private readonly ICustomersService customersService;

        public CoreOrder(IBooksService inputBooksService, ICustomersService inputCustomersService)
        {
            booksService = inputBooksService;
            customersService = inputCustomersService;
        }
        
        public async Task<(bool IsSuccess, IEnumerable<Order> Orders, string ErrorMessage)> GetOrdersAsync()
        {
            List<Order> lstOrder = new List<Order>();

            try
            {
                for (int i = 1; i < 6; i++)
                {
                    var objOrder = new Order();
                    objOrder.Id = i;
                    objOrder.OrderDate = DateTime.Now.AddDays(-i);
                    var resultCustomerService = await customersService.GetCustomer(i);
                    if (resultCustomerService.IsSuccess)
                    {
                        objOrder.EmailCustomer = resultCustomerService.ObjCustomer.Email;
                    }

                    var lstOrderItems = new List<OrderItem>();
                    for (int y = i; y < (i + 3); y++)
                    {
                        var orderItem = new OrderItem();
                        var resultBookService = await booksService.GetBook(y);
                        if (resultBookService.IsSuccess)
                        {
                            orderItem.BookTitle = resultBookService.ObjBook.Title;
                            orderItem.Price = resultBookService.ObjBook.Price;
                            orderItem.Quantity = y;
                            objOrder.Total = objOrder.Total + (orderItem.Price * orderItem.Quantity);
                        }
                        lstOrderItems.Add(orderItem);
                    }

                    objOrder.Books = new List<OrderItem>();
                    objOrder.Books.AddRange(lstOrderItems);

                    lstOrder.Add(objOrder);
                }

                return (true, lstOrder, null);
            }
            catch(Exception ex)
            {
                return (false, null, ex.Message);
            }
        }
    }
}


Before to test the new Microservice, we have to modify again the method ConfigureServices in Startup file, in order to add all the Classes created in the project and then, we have to add Api.Orders into Gateway:

[Startup.cs]

using Api.Orders.Core.Interfaces;
using Api.Orders.Core.Providers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;

namespace Api.Orders
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IBooksService, BooksService>();
            services.AddScoped<ICustomersService, CustomersService>();
            services.AddScoped<ICoreOrder, CoreOrder>();

            services.AddHttpClient("BookService", config =>
            {
                config.BaseAddress = new Uri(Configuration["Services:Books"]);
            });
            services.AddHttpClient("CustomerService", config =>
            {
                config.BaseAddress = new Uri(Configuration["Services:Customers"]);
            });

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}


[Ocelot.json]

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/books",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 56274
        }
      ],
      "UpstreamPathTemplate": "/api/books",
      "UpstreamHttpMethod": [ "GET" ]
    },
    {
      "DownstreamPathTemplate": "/api/books/{id}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 56274
        }
      ],
      "UpstreamPathTemplate": "/api/books/{id}",
      "UpstreamHttpMethod": [ "GET" ]
    },
    {
      "DownstreamPathTemplate": "/api/customers",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 56277
        }
      ],
      "UpstreamPathTemplate": "/api/customers",
      "UpstreamHttpMethod": [ "GET" ]
    },
    {
      "DownstreamPathTemplate": "/api/customers/{id}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 56277
        }
      ],
      "UpstreamPathTemplate": "/api/customers/{id}",
      "UpstreamHttpMethod": [ "GET" ]
    },
    {
      "DownstreamPathTemplate": "/api/orders",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 50730
        }
      ],
      "UpstreamPathTemplate": "/api/orders",
      "UpstreamHttpMethod": [ "GET" ]
    }
  ]
} 



Now, we run all the applications and, using Postman, we will check that everything works fine: