Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## 1.4.0 [unreleased]

### Features

1. [#174](https://github.com/InfluxCommunity/influxdb3-csharp/pull/174): Support passing HttpClient to InfluxDBClient.

## 1.3.0 [2025-08-12]

### Features
Expand Down
54 changes: 54 additions & 0 deletions Client.Test/InfluxDBClientWriteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using InfluxDB3.Client.Config;
using InfluxDB3.Client.Write;
Expand Down Expand Up @@ -490,4 +491,57 @@ public void WriteNoSyncTrueNotSupported()
"Server doesn't support write with NoSync=true (supported by InfluxDB 3 Core/Enterprise servers only)."));
});
}

[Test]
public async Task TestSetHttpClient()
{
MockServer
.Given(Request.Create().WithPath("/api/v2/write").UsingPost())
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));

var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("my-user-agent");
httpClient.DefaultRequestHeaders.Add("X-Client-ID", "123");

_client = new InfluxDBClient(new ClientConfig
{
Host = MockServerUrl,
Token = "my-token",
Database = "my-database",
HttpClient = httpClient
});

await _client.WriteRecordAsync("mem,tag=a field=1");
var requests = MockServer.LogEntries.ToList();
using (Assert.EnterMultipleScope())
{
Assert.That(requests[0].RequestMessage.Headers?["User-Agent"].First(), Is.EqualTo("my-user-agent"));
Assert.That(requests[0].RequestMessage.Headers["X-Client-ID"].First(), Is.EqualTo("123"));
}
Assert.Pass();
}

[Test]
public void TestCheckHttpClientStillOpen()
{
MockServer
.Given(Request.Create().WithPath("/test").UsingGet())
.RespondWith(
Response.Create()
.WithStatusCode(HttpStatusCode.OK)
.WithBody("Still ok"));

var httpClient = new HttpClient(new HttpClientHandler());
_client = new InfluxDBClient(new ClientConfig
{
Host = MockServerUrl,
Token = "my-token",
Database = "my-database",
HttpClient = httpClient
});
_client.Dispose();

var httpResponseMessage = httpClient.Send(new HttpRequestMessage(HttpMethod.Get, "test"));
Assert.That(httpResponseMessage.Content.ReadAsStringAsync().Result, Is.EqualTo("Still ok"));
}
}
10 changes: 5 additions & 5 deletions Client.Test/Internal/FlightSqlClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void SetUp()
Timeout = TimeSpan.FromSeconds(45)
};

_flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateAndConfigureHttpClient(config));
_flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateOrGetHttpClient(config));
}

[TearDown]
Expand Down Expand Up @@ -108,7 +108,7 @@ public void HeadersMetadataFromConfig()
}
};

_flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateAndConfigureHttpClient(config));
_flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateOrGetHttpClient(config));

var prepareHeadersMetadata =
_flightSqlClient.PrepareHeadersMetadata(new Dictionary<string, string>());
Expand Down Expand Up @@ -139,7 +139,7 @@ public void HeadersMetadataFromRequestArePreferred()
}
};

_flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateAndConfigureHttpClient(config));
_flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateOrGetHttpClient(config));

var prepareHeadersMetadata =
_flightSqlClient.PrepareHeadersMetadata(new Dictionary<string, string> { { "X-Tracing-Id", "258" } });
Expand Down Expand Up @@ -170,7 +170,7 @@ public void UserAgentHeaderNotChanged()
}
};

_flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateAndConfigureHttpClient(config));
_flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateOrGetHttpClient(config));

var prepareHeadersMetadata =
_flightSqlClient.PrepareHeadersMetadata(new Dictionary<string, string> { { "user-agent", "another/user-agent" } });
Expand Down Expand Up @@ -199,7 +199,7 @@ public void TestGrpcCallOptions()
};

Assert.DoesNotThrow(() =>
_flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateAndConfigureHttpClient(config)));
_flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateOrGetHttpClient(config)));
}

}
2 changes: 1 addition & 1 deletion Client.Test/Internal/RestClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ public void Timeout()

private void CreateAndConfigureRestClient(ClientConfig config)
{
_httpClient = InfluxDBClient.CreateAndConfigureHttpClient(config);
_httpClient = InfluxDBClient.CreateOrGetHttpClient(config);
_client = new RestClient(config, _httpClient);
}

Expand Down
2 changes: 1 addition & 1 deletion Client.Test/MockHttpsServerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace InfluxDB3.Client.Test;

public class MockHttpsServerTest
public abstract class MockHttpsServerTest
{
internal WireMockServer MockHttpsServer;
internal string MockHttpsServerUrl;
Expand Down
2 changes: 1 addition & 1 deletion Client.Test/MockServerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace InfluxDB3.Client.Test;

public class MockServerTest
public abstract class MockServerTest
{
internal WireMockServer MockServer, MockProxy;
internal string MockServerUrl, MockProxyUrl;
Expand Down
9 changes: 9 additions & 0 deletions Client/Config/ClientConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Web;
using InfluxDB3.Client.Write;

Expand All @@ -26,6 +27,7 @@ namespace InfluxDB3.Client.Config;
/// <item>- Proxy: The HTTP proxy URL. Default is not set.</item>
/// <item>- WriteOptions: Write options.</item>
/// <item>- QueryOptions Query options.</item>
/// <item>- HttpClient: The HttpClient will be used for Write and Query apis.</item>
/// </list>
///
/// <para>If you want create client with custom options, you can use the following code:</para>
Expand Down Expand Up @@ -200,6 +202,13 @@ public string Host
/// </summary>
public QueryOptions QueryOptions { get; set; }

/// <summary>
/// User-defined HttpClient.
/// Influxdb client will add an authentication header and base url to HttpClient. The rest is up to the users.
/// Users will be responsible for closing the HttpClient.
/// </summary>
public HttpClient? HttpClient { get; set; }

internal void Validate()
{
if (string.IsNullOrEmpty(Host))
Expand Down
44 changes: 35 additions & 9 deletions Client/InfluxDBClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ public InfluxDBClient(ClientConfig config)
config.Validate();

_config = config;
_httpClient = CreateAndConfigureHttpClient(_config);
_httpClient = CreateOrGetHttpClient(_config);
FlightSqlClient = new FlightSqlClient(config: _config, httpClient: _httpClient);
_restClient = new RestClient(config: _config, httpClient: _httpClient);
_gzipHandler = new GzipHandler(config.WriteOptions != null ? config.WriteOptions.GzipThreshold : 0);
Expand Down Expand Up @@ -840,7 +840,12 @@ await _restClient

public void Dispose()
{
_httpClient.Dispose();
// _config.HttpClient == null means HttpClient is created by the library, not from the user.
// so the client will be responsible for disposing of the HttpClient.
if (_config.HttpClient == null)
{
_httpClient.Dispose();
}
FlightSqlClient.Dispose();
_disposed = true;
}
Expand Down Expand Up @@ -876,7 +881,28 @@ private static StringBuilder ToLineProtocolBody(IEnumerable<object?> data, Write
return sb;
}

internal static HttpClient CreateAndConfigureHttpClient(ClientConfig config)
internal static HttpClient CreateOrGetHttpClient(ClientConfig config)
{
var httpClient = config.HttpClient;
if (httpClient == null)
{
httpClient = CreateHttpClient(config);
}

if (httpClient.BaseAddress == null)
{
httpClient.BaseAddress = new Uri(config.Host);
}

if (!string.IsNullOrEmpty(config.Token))
{
_setAuthenticationHeader(httpClient, config);
}

return httpClient;
}

private static HttpClient CreateHttpClient(ClientConfig config)
{
var handler = new HttpClientHandler();
if (handler.SupportsRedirectConfiguration)
Expand Down Expand Up @@ -916,17 +942,17 @@ internal static HttpClient CreateAndConfigureHttpClient(ClientConfig config)
{
Timeout = config.Timeout
};

client.DefaultRequestHeaders.UserAgent.ParseAdd(AssemblyHelper.GetUserAgent());
if (!string.IsNullOrEmpty(config.Token))
{
string authScheme = string.IsNullOrEmpty(config.AuthScheme) ? "Token" : config.AuthScheme!;
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, config.Token);
}

return client;
}

private static void _setAuthenticationHeader(HttpClient httpClient, ClientConfig config)
{
var authScheme = string.IsNullOrEmpty(config.AuthScheme) ? "Token" : config.AuthScheme!;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, config.Token);
}

private static string OptionMessage(string property)
{
return $"Please specify the '{property}' as a method parameter or use default configuration " +
Expand Down
Loading