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
Models
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
namespace Annytab.Fortnox.Client.V3
{
public class FortnoxOptions
{
#region Variables
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string AuthorizationCode { get; set; }
public string AccessToken { get; set; }
#endregion
#region Constructors
public FortnoxOptions()
{
// Set values for instance variables
this.ClientId = "";
this.ClientSecret = "";
this.AuthorizationCode = "";
this.AccessToken = "";
} // End of the constructor
#endregion
} // End of the class
} // End of the namespace
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
Thanks your post but i dont do appsettings.json file configure
Hi!
You can add options like this:
services.Configure<FortnoxOptions>(options => { options.ClientSecret = "1fBN6P7jRA"; options.AccessToken = "asdfasdfasdfasdfasdf"; });
nice post. could you please post the complete repository layer code?
Hi,
thank you for your comment. You can find all code on GitHub, a-fortnox-client.