Skip to content

Commit fae09a2

Browse files
authored
Add channel setting for disposing HttpClient (#458)
1 parent cfa82e0 commit fae09a2

File tree

10 files changed

+214
-48
lines changed

10 files changed

+214
-48
lines changed

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"sdk": {
3-
"version": "3.0.100-preview9-013906"
3+
"version": "3.0.100-preview9-013927"
44
}
55
}

src/Grpc.Net.Client/GrpcChannel.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ namespace Grpc.Net.Client
3636
/// Client objects can reuse the same channel. Creating a channel is an expensive operation compared to invoking
3737
/// a remote call so in general you should reuse a single channel for as many calls as possible.
3838
/// </summary>
39-
public sealed class GrpcChannel : ChannelBase
39+
public sealed class GrpcChannel : ChannelBase, IDisposable
4040
{
4141
internal const int DefaultMaxReceiveMessageSize = 1024 * 1024 * 4; // 4 MB
4242

@@ -49,13 +49,21 @@ public sealed class GrpcChannel : ChannelBase
4949
internal List<CallCredentials>? CallCredentials { get; }
5050
internal Dictionary<string, ICompressionProvider> CompressionProviders { get; }
5151
internal string MessageAcceptEncoding { get; }
52+
internal bool Disposed { get; private set; }
5253

5354
// Timing related options that are set in unit tests
5455
internal ISystemClock Clock = SystemClock.Instance;
5556
internal bool DisableClientDeadlineTimer { get; set; }
5657

58+
private bool _shouldDisposeHttpClient;
59+
5760
internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(address.Authority)
5861
{
62+
// Dispose the HttpClient if...
63+
// 1. No client was specified and so the channel created the HttpClient itself
64+
// 2. User has specified a client and set DisposeHttpClient to true
65+
_shouldDisposeHttpClient = channelOptions.HttpClient == null || channelOptions.DisposeHttpClient;
66+
5967
Address = address;
6068
HttpClient = channelOptions.HttpClient ?? new HttpClient();
6169
SendMaxMessageSize = channelOptions.MaxSendMessageSize;
@@ -117,6 +125,11 @@ private void ValidateChannelCredentials()
117125
/// <returns>A new <see cref="CallInvoker"/>.</returns>
118126
public override CallInvoker CreateCallInvoker()
119127
{
128+
if (Disposed)
129+
{
130+
throw new ObjectDisposedException(nameof(GrpcChannel));
131+
}
132+
120133
var invoker = new HttpClientCallInvoker(this);
121134

122135
return invoker;
@@ -211,5 +224,23 @@ public static GrpcChannel ForAddress(Uri address, GrpcChannelOptions channelOpti
211224

212225
return new GrpcChannel(address, channelOptions);
213226
}
227+
228+
/// <summary>
229+
/// Releases the resources used by the <see cref="GrpcChannel"/> class.
230+
/// Clients created with the channel can't be used after the channel is disposed.
231+
/// </summary>
232+
public void Dispose()
233+
{
234+
if (Disposed)
235+
{
236+
return;
237+
}
238+
239+
if (_shouldDisposeHttpClient)
240+
{
241+
HttpClient.Dispose();
242+
}
243+
Disposed = true;
244+
}
214245
}
215246
}

src/Grpc.Net.Client/GrpcChannelOptions.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
#endregion
1818

19-
using System.Collections;
2019
using System.Collections.Generic;
2120
using System.Net.Http;
2221
using Grpc.Core;
@@ -58,10 +57,26 @@ public sealed class GrpcChannelOptions
5857
/// <summary>
5958
/// Gets or sets the <see cref="HttpClient"/> used by the channel.
6059
/// </summary>
60+
/// <remarks>
61+
/// By default a <see cref="System.Net.Http.HttpClient"/> specified here will not be disposed with the channel.
62+
/// To dispose the <see cref="System.Net.Http.HttpClient"/> with the channel you must set <see cref="DisposeHttpClient"/>
63+
/// to <c>true</c>.
64+
/// </remarks>
6165
public HttpClient? HttpClient { get; set; }
6266

6367
/// <summary>
64-
///
68+
/// Gets or sets a value indicating whether the underlying <see cref="System.Net.Http.HttpClient"/> should be disposed
69+
/// when the <see cref="GrpcChannel"/> instance is disposed. The default value is <c>false</c>.
70+
/// </summary>
71+
/// <remarks>
72+
/// This setting is used when a <see cref="HttpClient"/> value is specified. If no <see cref="HttpClient"/> value is provided
73+
/// then the channel will create an <see cref="System.Net.Http.HttpClient"/> instance that is always disposed when
74+
/// the channel is disposed.
75+
/// </remarks>
76+
public bool DisposeHttpClient { get; set; }
77+
78+
/// <summary>
79+
/// Initializes a new instance of the <see cref="GrpcChannelOptions"/> class.
6580
/// </summary>
6681
public GrpcChannelOptions()
6782
{

src/Grpc.Net.Client/Internal/HttpClientCallInvoker.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ private GrpcCall<TRequest, TResponse> CreateGrpcCall<TRequest, TResponse>(
119119
where TRequest : class
120120
where TResponse : class
121121
{
122+
if (Channel.Disposed)
123+
{
124+
throw new ObjectDisposedException(nameof(GrpcChannel));
125+
}
126+
122127
var call = new GrpcCall<TRequest, TResponse>(method, options, Channel);
123128

124129
return call;

test/FunctionalTests/Client/EventSourceTests.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ async Task<HelloReply> UnarySuccess(HelloRequest request, ServerCallContext cont
6464
// Act - Start call
6565
var method = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(UnarySuccess);
6666

67-
var client = TestClientFactory.Create(Fixture.Client, LoggerFactory, method);
67+
var client = TestClientFactory.Create(Channel, method);
6868

6969
var call = client.UnaryCall(new HelloRequest());
7070

@@ -131,7 +131,7 @@ async Task<HelloReply> UnaryError(HelloRequest request, ServerCallContext contex
131131
// Act - Start call
132132
var method = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(UnaryError);
133133

134-
var client = TestClientFactory.Create(Fixture.Client, LoggerFactory, method);
134+
var client = TestClientFactory.Create(Channel, method);
135135

136136
var call = client.UnaryCall(new HelloRequest());
137137

@@ -192,7 +192,10 @@ async Task<HelloReply> UnaryDeadlineExceeded(HelloRequest request, ServerCallCon
192192
// Act - Start call
193193
var method = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(UnaryDeadlineExceeded);
194194

195-
var client = TestClientFactory.Create(Fixture.Client, LoggerFactory, method, disableClientDeadlineTimer: true);
195+
var channel = CreateChannel();
196+
channel.DisableClientDeadlineTimer = true;
197+
198+
var client = TestClientFactory.Create(Channel, method);
196199

197200
var call = client.UnaryCall(new HelloRequest(), new CallOptions(deadline: DateTime.UtcNow.AddMilliseconds(200)));
198201

@@ -249,7 +252,7 @@ async Task DuplexStreamingMethod(IAsyncStreamReader<HelloRequest> requestStream,
249252
// Act - Start call
250253
var method = Fixture.DynamicGrpc.AddDuplexStreamingMethod<HelloRequest, HelloReply>(DuplexStreamingMethod);
251254

252-
var client = TestClientFactory.Create(Fixture.Client, LoggerFactory, method);
255+
var client = TestClientFactory.Create(Channel, method);
253256

254257
var call = client.DuplexStreamingCall();
255258

test/FunctionalTests/Client/MaxMessageSizeTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using Greet;
2121
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
2222
using Grpc.Core;
23+
using Grpc.Net.Client;
2324
using Grpc.Tests.Shared;
2425
using NUnit.Framework;
2526

@@ -52,7 +53,10 @@ Task<HelloReply> UnaryDeadlineExceeded(HelloRequest request, ServerCallContext c
5253

5354
var method = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(UnaryDeadlineExceeded);
5455

55-
var client = TestClientFactory.Create(Fixture.Client, LoggerFactory, method, disableClientDeadlineTimer: true);
56+
var channel = CreateChannel();
57+
channel.DisableClientDeadlineTimer = true;
58+
59+
var client = TestClientFactory.Create(channel, method);
5660

5761
// Act
5862
var ex = await ExceptionAssert.ThrowsAsync<RpcException>(() => client.UnaryCall(new HelloRequest()).ResponseAsync).DefaultTimeout();

test/FunctionalTests/Client/StreamingTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
using System.Threading.Tasks;
2424
using Google.Protobuf;
2525
using Grpc.Core;
26-
using Grpc.Net.Client;
2726
using Grpc.Tests.Shared;
2827
using Microsoft.Extensions.Logging;
2928
using NUnit.Framework;

test/FunctionalTests/FunctionalTestBase.cs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,15 @@ public class FunctionalTestBase
3838

3939
protected ILogger Logger => _testContext!.Logger;
4040

41-
protected GrpcChannel Channel
41+
protected GrpcChannel Channel => _channel ??= CreateChannel();
42+
43+
protected GrpcChannel CreateChannel()
4244
{
43-
get
45+
return GrpcChannel.ForAddress(Fixture.Client.BaseAddress, new GrpcChannelOptions
4446
{
45-
if (_channel == null)
46-
{
47-
_channel = GrpcChannel.ForAddress(Fixture.Client.BaseAddress, new GrpcChannelOptions
48-
{
49-
LoggerFactory = LoggerFactory,
50-
HttpClient = Fixture.Client
51-
});
52-
}
53-
54-
return _channel;
55-
}
47+
LoggerFactory = LoggerFactory,
48+
HttpClient = Fixture.Client
49+
});
5650
}
5751

5852
protected virtual void ConfigureServices(IServiceCollection services) { }

test/FunctionalTests/Infrastructure/TestClient.cs

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,12 @@ internal class TestClient<TRequest, TResponse> : ClientBase
2828
where TRequest : class
2929
where TResponse : class
3030
{
31-
private readonly HttpClientCallInvoker _callInvoker;
31+
private readonly CallInvoker _callInvoker;
3232
private readonly Method<TRequest, TResponse> _method;
3333

34-
public TestClient(HttpClient httpClient, ILoggerFactory loggerFactory, Method<TRequest, TResponse> method, bool disableClientDeadlineTimer = false)
34+
public TestClient(ChannelBase channel, Method<TRequest, TResponse> method)
3535
{
36-
var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions
37-
{
38-
LoggerFactory = loggerFactory,
39-
HttpClient = httpClient
40-
});
41-
channel.DisableClientDeadlineTimer = disableClientDeadlineTimer;
42-
43-
_callInvoker = new HttpClientCallInvoker(channel);
36+
_callInvoker = channel.CreateCallInvoker();
4437
_method = method;
4538
}
4639

@@ -68,14 +61,12 @@ public AsyncDuplexStreamingCall<TRequest, TResponse> DuplexStreamingCall(CallOpt
6861
internal static class TestClientFactory
6962
{
7063
public static TestClient<TRequest, TResponse> Create<TRequest, TResponse>(
71-
HttpClient httpClient,
72-
ILoggerFactory loggerFactory,
73-
Method<TRequest, TResponse> method,
74-
bool disableClientDeadlineTimer = false)
64+
ChannelBase channel,
65+
Method<TRequest, TResponse> method)
7566
where TRequest : class
7667
where TResponse : class
7768
{
78-
return new TestClient<TRequest, TResponse>(httpClient, loggerFactory, method, disableClientDeadlineTimer);
69+
return new TestClient<TRequest, TResponse>(channel, method);
7970
}
8071
}
8172
}

0 commit comments

Comments
 (0)