Generic HttpClient in ASP.NET Core

In this post we will describe how you can create a typed generic HttpClient in ASP.NET Core. A typed generic HttpClient might be useful if you want to connect to an Api that uses the same method names in all endpoints.

A HttpClient is used to send HTTP requests to an url and to receive HTTP responses from an url. You can use an HttpClient to connect to different Api:s. Our HttpClient should be used to make calls to methods in the Fortnox Api.

It is not recommended to create and dispose of HttpClients in your program. You should use static http clients or IHttpClientFactory. IHttpClientFactory was added in ASP.NET Core 2.1 and is used to handle the creation of HttpClients in an efficient manor. You can register named clients and typed clients in ASP.NET Core to be able to manage your clients in a convenient way. You can also create new clients without a name from IHttpClientFactory.

Interface

We have created an generic interface for our typed HttpClient. The generic data type is designated as R (Root), it can be any letter or word. A class that implements this interface can be asynchronous, all methods in this interface returns a Task.

public interface IFortnoxClient
{
    Task<FortnoxResponse<R>> Add<R>(R root, string uri);
    Task<FortnoxResponse<R>> Update<R>(R root, string uri);
    Task<FortnoxResponse<R>> Action<R>(string uri);
    Task<FortnoxResponse<R>> Get<R>(string uri);
    Task<FortnoxResponse<bool>> Delete(string uri);
    Task<FortnoxResponse<R>> UploadFile<R>(Stream stream, string file_name, string uri);
    Task<FortnoxResponse<bool>> DownloadFile(Stream stream, string uri);

} // End of the interface

We have used a FortnoxResponse as a model around our generic data type, this makes it possible to return multiple objects/values from methods in classes that implements this interface.

public class FortnoxResponse<R>
{
    #region Variables

    public R model { get; set; }
    public string error { get; set; }

    #endregion

    #region Constructors

    public FortnoxResponse()
    {
        // Set values for instance variables
        this.model = default(R);
        this.error = null;

    } // End of the constructor

    #endregion

    #region Get methods

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);

    } // End of the ToString method

    #endregion

} // End of the class

Generic Fortnox Client

Our generic fortnox client includes all the methods that we need to communicate with Fortnox Api. All methods take a uri as a parameter and can handle different data types as input and output. This client depends on a HttpClient and FortnoxOptions, this is a typed client because it sets values for the HttpClient in the constructor.

public class FortnoxClient : IFortnoxClient
{
    #region Variables

    private readonly HttpClient client;
    private readonly FortnoxOptions options;

    #endregion

    #region Constructors

    public FortnoxClient(HttpClient http_client, IOptions<FortnoxOptions> options)
    {
        // Set values for instance variables
        this.client = http_client;
        this.options = options.Value;

        // Set values for the client
        this.client.BaseAddress = new Uri("https://api.fortnox.se/3/");
        this.client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        this.client.DefaultRequestHeaders.Add("Client-Secret", this.options.ClientSecret);
        this.client.DefaultRequestHeaders.Add("Access-Token", this.options.AccessToken);
            
    } // End of the constructor

    #endregion

    #region Add methods

    public async Task<FortnoxResponse<R>> Add<R>(R root, string uri)
    {
        // Convert the post to json
        string json = JsonConvert.SerializeObject(root, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });

        // Create the response to return
        FortnoxResponse<R> fr = new FortnoxResponse<R>();

        // Send data as application/json data
        using (StringContent content = new StringContent(json, Encoding.UTF8, "application/json"))
        {
            try
            {
                // Get the response
                HttpResponseMessage response = await this.client.PostAsync(uri, content);

                // Check the status code for the response
                if (response.IsSuccessStatusCode == true)
                {
                    // Get string data
                    string data = await response.Content.ReadAsStringAsync();

                    // Deserialize the data
                    fr.model = JsonConvert.DeserializeObject<R>(data);
                }
                else
                {
                    // Get string data
                    string data = await response.Content.ReadAsStringAsync();

                    // Add error data
                    fr.error = $"Add: {uri}. {Regex.Unescape(data)}";
                }
            }
            catch (Exception ex)
            {
                // Add exception data
                fr.error = $"Add: {uri}. {ex.ToString()}";
            }
        }

        // Return the response
        return fr;

    } // End of the Add method

    #endregion

    #region Update methods

    public async Task<FortnoxResponse<R>> Update<R>(R root, string uri)
    {
        // Convert the post to json
        string json = JsonConvert.SerializeObject(root, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });

        // Create the response to return
        FortnoxResponse<R> fr = new FortnoxResponse<R>();

        // Send data as application/json data
        using (StringContent content = new StringContent(json, Encoding.UTF8, "application/json"))
        {
            try
            {
                // Get the response
                HttpResponseMessage response = await this.client.PutAsync(uri, content);

                // Check the status code for the response
                if (response.IsSuccessStatusCode == true)
                {
                    // Get string data
                    string data = await response.Content.ReadAsStringAsync();

                    // Deserialize the data
                    fr.model = JsonConvert.DeserializeObject<R>(data);
                }
                else
                {
                    // Get string data
                    string data = await response.Content.ReadAsStringAsync();

                    // Add error data
                    fr.error = $"Update: {uri}. {Regex.Unescape(data)}";
                }
            }
            catch (Exception ex)
            {
                // Add exception data
                fr.error = $"Update: {uri}. {ex.ToString()}";
            }
        }

        // Return the response
        return fr;

    } // End of the Update method

    public async Task<FortnoxResponse<R>> Action<R>(string uri)
    {
        // Create the response to return
        FortnoxResponse<R> fr = new FortnoxResponse<R>();

        try
        {
            // Get the response
            HttpResponseMessage response = await this.client.PutAsync(uri, null);

            // Check the status code for the response
            if (response.IsSuccessStatusCode == true)
            {
                // Get string data
                string data = await response.Content.ReadAsStringAsync();

                // Deserialize the data
                fr.model = JsonConvert.DeserializeObject<R>(data);
            }
            else
            {
                // Get string data
                string data = await response.Content.ReadAsStringAsync();

                // Add error data
                fr.error = $"Action: {uri}. {Regex.Unescape(data)}";
            }
        }
        catch (Exception ex)
        {
            // Add exception data
            fr.error = $"Action: {uri}. {ex.ToString()}";
        }

        // Return the response
        return fr;

    } // End of the Action method

    #endregion

    #region Get methods

    public async Task<FortnoxResponse<R>> Get<R>(string uri)
    {
        // Create the response to return
        FortnoxResponse<R> fr = new FortnoxResponse<R>();

        try
        {
            // Get the response
            HttpResponseMessage response = await this.client.GetAsync(uri);

            // Check the status code for the response
            if (response.IsSuccessStatusCode == true)
            {
                // Get string data
                string data = await response.Content.ReadAsStringAsync();

                // Deserialize the data
                fr.model = JsonConvert.DeserializeObject<R>(data);
            }
            else
            {
                // Get string data
                string data = await response.Content.ReadAsStringAsync();

                // Add error data
                fr.error = $"Get: {uri}. {Regex.Unescape(data)}";
            }
        }
        catch (Exception ex)
        {
            // Add exception data
            fr.error = $"Get: {uri}. {ex.ToString()}";
        }

        // Return the post
        return fr;

    } // End of the Get method

    #endregion

    #region Delete methods

    public async Task<FortnoxResponse<bool>> Delete(string uri)
    {
        // Create the response to return
        FortnoxResponse<bool> fr = new FortnoxResponse<bool>();

        // Indicate success
        fr.model = true;

        try
        {
            // Get the response
            HttpResponseMessage response = await this.client.DeleteAsync(uri);

            // Check the status code for the response
            if (response.IsSuccessStatusCode == false)
            {
                // Get string data
                string data = await response.Content.ReadAsStringAsync();

                // Add error data
                fr.model = false;
                fr.error = $"Delete: {uri}. {Regex.Unescape(data)}";
            }
        }
        catch (Exception ex)
        {
            // Add exception data
            fr.model = false;
            fr.error = $"Delete: {uri}. {ex.ToString()}";
        }

        // Return the response
        return fr;

    } // End of the Delete method

    #endregion

    #region File methods

    public async Task<FortnoxResponse<R>> UploadFile<R>(Stream stream, string file_name, string uri)
    {
        // Create the response to return
        FortnoxResponse<R> fr = new FortnoxResponse<R>();

        // Send data as multipart/form-data content
        using (MultipartFormDataContent content = new MultipartFormDataContent())
        {
            // Add content
            content.Add(new StreamContent(stream), "file", file_name);

            try
            {
                // Get the response
                HttpResponseMessage response = await this.client.PostAsync(uri, content);

                // Check the status code for the response
                if (response.IsSuccessStatusCode == true)
                {
                    // Get the data
                    string data = await response.Content.ReadAsStringAsync();

                    // Deserialize the data
                    fr.model = JsonConvert.DeserializeObject<R>(data);
                }
                else
                {
                    // Get string data
                    string data = await response.Content.ReadAsStringAsync();

                    // Add error data
                    fr.error = $"UploadFile: {uri}. {Regex.Unescape(data)}";
                }
            }
            catch (Exception ex)
            {
                // Add exception data
                fr.error = $"UploadFile: {uri}. {ex.ToString()}";
            }
        }

        // Return the response
        return fr;

    } // End of the UploadFile method

    public async Task<FortnoxResponse<bool>> DownloadFile(Stream stream, string uri)
    {
        // Create the response to return
        FortnoxResponse<bool> fr = new FortnoxResponse<bool>();

        // Indicate success
        fr.model = true;

        try
        {
            // Get the response
            HttpResponseMessage response = await this.client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);

            // Check the status code for the response
            if (response.IsSuccessStatusCode == true)
            {
                // Get the stream
                await response.Content.CopyToAsync(stream);
            }
            else
            {
                // Get string data
                string data = await response.Content.ReadAsStringAsync();

                // Add error data
                fr.model = false;
                fr.error = $"DownloadFile: {uri}. {Regex.Unescape(data)}";
            }
        }
        catch (Exception ex)
        {
            // Add exception data
            fr.model = false;
            fr.error = $"DownloadFile: {uri}. {ex.ToString()}";
        }

        // Return the response
        return fr;

    } // End of the DownloadFile method

    #endregion

} // End of the class

Services

You can register FortnoxOptions and the typed FortnoxClient as services in the ConfigureServices method in the StartUp class of your project. When you call AddHttpClient you will also add IHttpClientFactory to your service container. FortnoxOptions is created directly from the appsettings.json file. We also add a client handler with automatic decompression to the typed client, this will also add headers to accept gzip or deflate.

// Create api options
services.Configure<FortnoxOptions>(configuration.GetSection("FortnoxOptions"));

// Add clients
services.AddHttpClient<IFortnoxClient, FortnoxClient>().ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate });

You can also create a FortnoxClient by using the constructor. We create an empty client from IHttpClientFactory as a parameter in the constructor, use a named client if you want to add automatic decompression.

// Create api options
IOptions<FortnoxOptions> options = Options.Create<FortnoxOptions>(new FortnoxOptions
{
	ClientSecret = "1fBN6P7jRA",
	AccessToken = "asdfasdfasdfasdfasdf"
});

// Create a fortnox client
IFortnoxClient fortnox_client = new FortnoxClient(this.client_factory.CreateClient(), options);

How to use the client

To use the client we first need models, you can check out our a-fortnox-client repository on GitHub for more examples of models.

public class ModesOfPaymentsRoot
{
    #region Variables

    public IList<ModeOfPayment> ModesOfPayments { get; set; }
    public MetaInformation MetaInformation { get; set; }

    #endregion

    #region Constructors

    public ModesOfPaymentsRoot()
    {
        this.ModesOfPayments = null;
        this.MetaInformation = null;

    } // End of the constructor

    #endregion

} // End of the class

public class ModeOfPaymentRoot
{
    #region Variables

    public ModeOfPayment ModeOfPayment { get; set; }

    #endregion

    #region Constructors

    public ModeOfPaymentRoot()
    {
        this.ModeOfPayment = null;

    } // End of the constructor

    #endregion

} // End of the class

public class ModeOfPayment
{
    #region Variables

    [JsonProperty("@url")]
    public string Url { get; set; }
    public string Code { get; set; }
    public string Description { get; set; }
    public string AccountNumber { get; set; }

    #endregion

    #region Constructors

    public ModeOfPayment()
    {
        // Set values for instance variables
        this.Url = null;
        this.Code = null;
        this.Description = null;
        this.AccountNumber = null;

    } // End of the constructor

    #endregion

} // End of the class

We have models for mode of payments and are now able to use our client to add, update and get modes of payment.

public async Task TestAddPost()
{
    // Create a post
    ModeOfPaymentRoot post = new ModeOfPaymentRoot
    {
        ModeOfPayment = new ModeOfPayment
        {
            Code = "LB",
            Description = "Bankgiro LB",
            AccountNumber = "1940"
        }
    };

    // Add the post
    FortnoxResponse<ModeOfPaymentRoot> fr = await this.fortnox_client.Add<ModeOfPaymentRoot>(post, "modesofpayments");

} // End of the TestAddPost method

public async Task TestUpdatePost()
{
    // Create a post
    ModeOfPaymentRoot post = new ModeOfPaymentRoot
    {
        ModeOfPayment = new ModeOfPayment
        {
            Code = "LB",
            Description = "Bankgiro LB",
            AccountNumber = "1930"
        }
    };

    // Update the post
    FortnoxResponse<ModeOfPaymentRoot> fr = await this.fortnox_client.Update<ModeOfPaymentRoot>(post, "modesofpayments/LB");

} // End of the TestUpdatePost method

public async Task TestGetPost()
{
    // Get a post
    FortnoxResponse<ModeOfPaymentRoot> fr = await this.fortnox_client.Get<ModeOfPaymentRoot>("modesofpayments/LB");

} // End of the TestGetPost method

public async Task TestGetList()
{
    // Get a list
    FortnoxResponse<ModesOfPaymentsRoot> fr = await this.fortnox_client.Get<ModesOfPaymentsRoot>("modesofpayments?limit=2&page=1");

} // End of the TestGetList method

Leave a Reply

Your email address will not be published. Required fields are marked *