Skip to content

Commit 97c40b5

Browse files
committed
Make FacturapiClient the disposable entry point and internalize wrappers
1 parent e09eea0 commit 97c40b5

13 files changed

+530
-333
lines changed

CHANGELOG.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [4.12.0] = Unreleased
8+
## [5.0.0] - Unreleased
9+
10+
### Breaking
11+
12+
- Wrappers can no longer be constructed directly; their constructors are internal and they are intended to be used only through `FacturapiClient`.
913

1014
### Added
1115

1216
- `FacturapiException.Status` now surfaces the HTTP status code when available.
1317

18+
### Changed
19+
20+
- `FacturapiClient` now implements `IDisposable`; dispose it when finished to release HTTP resources. If not disposed, garbage collection will eventually clean up, but explicit disposal avoids lingering HTTP connections.
21+
1422
## [4.11.0] - 2025-12-10
1523

1624
### Added
@@ -65,7 +73,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6573
- Type IepsMode for Tax model
6674
- Type Factor for Tax model
6775

68-
## [4.7.0] = 2025-02-25
76+
## [4.7.0] - 2025-02-25
6977

7078
### Added
7179

FacturapiClient.cs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
namespace Facturapi
1+
using System;
2+
using System.Net.Http;
3+
using System.Net.Http.Headers;
4+
using System.Text;
5+
6+
namespace Facturapi
27
{
3-
public class FacturapiClient
8+
public class FacturapiClient : IDisposable
49
{
510
public Wrappers.CustomerWrapper Customer { get; private set; }
611
public Wrappers.ProductWrapper Product { get; private set; }
@@ -11,18 +16,31 @@ public class FacturapiClient
1116
public Wrappers.CatalogWrapper Catalog { get; private set; }
1217
public Wrappers.CatalogWrapper CartaporteCatalog { get; private set; }
1318
public Wrappers.ToolWrapper Tool { get; private set; }
19+
private readonly HttpClient httpClient;
1420

1521
public FacturapiClient(string apiKey, string apiVersion = "v2")
1622
{
17-
this.Customer = new Wrappers.CustomerWrapper(apiKey, apiVersion);
18-
this.Product = new Wrappers.ProductWrapper(apiKey, apiVersion);
19-
this.Invoice = new Wrappers.InvoiceWrapper(apiKey, apiVersion);
20-
this.Organization = new Wrappers.OrganizationWrapper(apiKey, apiVersion);
21-
this.Receipt = new Wrappers.ReceiptWrapper(apiKey, apiVersion);
22-
this.Retention = new Wrappers.RetentionWrapper(apiKey, apiVersion);
23-
this.Catalog = new Wrappers.CatalogWrapper(apiKey, apiVersion);
24-
this.CartaporteCatalog = new Wrappers.CatalogWrapper(apiKey, apiVersion);
25-
this.Tool = new Wrappers.ToolWrapper(apiKey, apiVersion);
23+
var apiKeyBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(apiKey + ":"));
24+
this.httpClient = new HttpClient
25+
{
26+
BaseAddress = new Uri($"https://www.facturapi.io/{apiVersion}/")
27+
};
28+
this.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", apiKeyBase64);
29+
30+
this.Customer = new Wrappers.CustomerWrapper(apiKey, apiVersion, this.httpClient);
31+
this.Product = new Wrappers.ProductWrapper(apiKey, apiVersion, this.httpClient);
32+
this.Invoice = new Wrappers.InvoiceWrapper(apiKey, apiVersion, this.httpClient);
33+
this.Organization = new Wrappers.OrganizationWrapper(apiKey, apiVersion, this.httpClient);
34+
this.Receipt = new Wrappers.ReceiptWrapper(apiKey, apiVersion, this.httpClient);
35+
this.Retention = new Wrappers.RetentionWrapper(apiKey, apiVersion, this.httpClient);
36+
this.Catalog = new Wrappers.CatalogWrapper(apiKey, apiVersion, this.httpClient);
37+
this.CartaporteCatalog = new Wrappers.CatalogWrapper(apiKey, apiVersion, this.httpClient);
38+
this.Tool = new Wrappers.ToolWrapper(apiKey, apiVersion, this.httpClient);
39+
}
40+
41+
public void Dispose()
42+
{
43+
this.httpClient?.Dispose();
2644
}
2745
}
2846
}

Wrappers/BaseWrapper.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,17 @@ namespace Facturapi.Wrappers
99
{
1010
public abstract class BaseWrapper
1111
{
12-
protected const string BASE_URL = "https://www.facturapi.io/";
1312
protected HttpClient client;
1413
protected JsonSerializerSettings jsonSettings { get; set; }
1514
public string apiKey { get; set; }
1615
public string apiVersion { get; set; }
1716

18-
public BaseWrapper(string apiKey, string apiVersion = "v2")
17+
public BaseWrapper(string apiKey, string apiVersion, HttpClient httpClient)
1918
{
20-
var apiKeyBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(apiKey + ":"));
21-
this.client = new HttpClient()
22-
{
23-
BaseAddress = new Uri($"{BASE_URL}/{apiVersion}/")
24-
};
25-
this.client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", apiKeyBase64);
19+
this.apiKey = apiKey;
20+
this.apiVersion = apiVersion;
21+
22+
this.client = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
2623
this.jsonSettings = new JsonSerializerSettings
2724
{
2825
ContractResolver = new SnakeCasePropertyNamesContractResolver(),

Wrappers/CatalogWrapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Facturapi.Wrappers
77
{
88
public class CatalogWrapper : BaseWrapper
99
{
10-
public CatalogWrapper(string apiKey, string apiVersion = "v2") : base(apiKey, apiVersion)
10+
internal CatalogWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient)
1111
{
1212
}
1313

Wrappers/CustomerWrapper.cs

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,69 +8,86 @@ namespace Facturapi.Wrappers
88
{
99
public class CustomerWrapper : BaseWrapper
1010
{
11-
public CustomerWrapper(string apiKey, string apiVersion = "v2") : base(apiKey, apiVersion)
11+
internal CustomerWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient)
1212
{
1313
}
1414

1515
public async Task<SearchResult<Customer>> ListAsync(Dictionary<string, object> query = null)
1616
{
17-
var response = await client.GetAsync(Router.ListCustomers(query));
18-
await this.ThrowIfErrorAsync(response);
19-
var resultString = await response.Content.ReadAsStringAsync();
17+
using (var response = await client.GetAsync(Router.ListCustomers(query)))
18+
{
19+
await this.ThrowIfErrorAsync(response);
20+
var resultString = await response.Content.ReadAsStringAsync();
2021

21-
var searchResult = JsonConvert.DeserializeObject<SearchResult<Customer>>(resultString, this.jsonSettings);
22-
return searchResult;
22+
var searchResult = JsonConvert.DeserializeObject<SearchResult<Customer>>(resultString, this.jsonSettings);
23+
return searchResult;
24+
}
2325
}
2426

2527
public async Task<Customer> CreateAsync(Dictionary<string, object> data, Dictionary<string, object> queryParams = null)
2628
{
27-
var response = await client.PostAsync(Router.CreateCustomer(queryParams), new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"));
28-
await this.ThrowIfErrorAsync(response);
29-
var resultString = await response.Content.ReadAsStringAsync();
30-
var customer = JsonConvert.DeserializeObject<Customer>(resultString, this.jsonSettings);
31-
return customer;
29+
using (var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"))
30+
using (var response = await client.PostAsync(Router.CreateCustomer(queryParams), content))
31+
{
32+
await this.ThrowIfErrorAsync(response);
33+
var resultString = await response.Content.ReadAsStringAsync();
34+
var customer = JsonConvert.DeserializeObject<Customer>(resultString, this.jsonSettings);
35+
return customer;
36+
}
3237
}
3338

3439
public async Task<Customer> RetrieveAsync(string id)
3540
{
36-
var response = await client.GetAsync(Router.RetrieveCustomer(id));
37-
await this.ThrowIfErrorAsync(response);
38-
var resultString = await response.Content.ReadAsStringAsync();
39-
var customer = JsonConvert.DeserializeObject<Customer>(resultString, this.jsonSettings);
40-
return customer;
41+
using (var response = await client.GetAsync(Router.RetrieveCustomer(id)))
42+
{
43+
await this.ThrowIfErrorAsync(response);
44+
var resultString = await response.Content.ReadAsStringAsync();
45+
var customer = JsonConvert.DeserializeObject<Customer>(resultString, this.jsonSettings);
46+
return customer;
47+
}
4148
}
4249

4350
public async Task<Customer> DeleteAsync(string id)
4451
{
45-
var response = await client.DeleteAsync(Router.DeleteCustomer(id));
46-
await this.ThrowIfErrorAsync(response);
47-
var resultString = await response.Content.ReadAsStringAsync();
48-
var customer = JsonConvert.DeserializeObject<Customer>(resultString, this.jsonSettings);
49-
return customer;
52+
using (var response = await client.DeleteAsync(Router.DeleteCustomer(id)))
53+
{
54+
await this.ThrowIfErrorAsync(response);
55+
var resultString = await response.Content.ReadAsStringAsync();
56+
var customer = JsonConvert.DeserializeObject<Customer>(resultString, this.jsonSettings);
57+
return customer;
58+
}
5059
}
5160

5261
public async Task<Customer> UpdateAsync(string id, Dictionary<string, object> data, Dictionary<string, object> queryParams = null)
5362
{
54-
var response = await client.PutAsync(Router.UpdateCustomer(id, queryParams), new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"));
55-
await this.ThrowIfErrorAsync(response);
56-
var resultString = await response.Content.ReadAsStringAsync();
57-
var customer = JsonConvert.DeserializeObject<Customer>(resultString, this.jsonSettings);
58-
return customer;
63+
using (var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"))
64+
using (var response = await client.PutAsync(Router.UpdateCustomer(id, queryParams), content))
65+
{
66+
await this.ThrowIfErrorAsync(response);
67+
var resultString = await response.Content.ReadAsStringAsync();
68+
var customer = JsonConvert.DeserializeObject<Customer>(resultString, this.jsonSettings);
69+
return customer;
70+
}
5971
}
6072

6173
public async Task<TaxInfoValidation> ValidateTaxInfoAsync(string id)
6274
{
63-
var response = await client.GetAsync(Router.ValidateCustomerTaxInfo(id));
64-
await this.ThrowIfErrorAsync(response);
65-
var resultString = await response.Content.ReadAsStringAsync();
66-
var validation = JsonConvert.DeserializeObject<TaxInfoValidation>(resultString, this.jsonSettings);
67-
return validation;
75+
using (var response = await client.GetAsync(Router.ValidateCustomerTaxInfo(id)))
76+
{
77+
await this.ThrowIfErrorAsync(response);
78+
var resultString = await response.Content.ReadAsStringAsync();
79+
var validation = JsonConvert.DeserializeObject<TaxInfoValidation>(resultString, this.jsonSettings);
80+
return validation;
81+
}
6882
}
6983

7084
public async Task SendEditLinkByEmailAsync(string id, Dictionary<string, object> data)
7185
{
72-
var response = await client.PostAsync(Router.SendEditLinkByEmail(id), new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"));
73-
await this.ThrowIfErrorAsync(response);
86+
using (var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"))
87+
using (var response = await client.PostAsync(Router.SendEditLinkByEmail(id), content))
88+
{
89+
await this.ThrowIfErrorAsync(response);
90+
}
7491
}
7592
}
7693
}

0 commit comments

Comments
 (0)