Skip to content

Commit 8257b37

Browse files
authored
Add the ability to register Refit clients as keyed services (#1981)
* Add the ability to register Refit clients as keyed services * Ensure that we have a HttpClient per service key This can be achieved by adding `$";{serviceKey}"` to the `httpClientName` * Second take to fix the HTTP client name * Better output
1 parent a20dbb2 commit 8257b37

File tree

5 files changed

+310
-0
lines changed

5 files changed

+310
-0
lines changed

Refit.HttpClientFactory/HttpClientFactoryExtensions.cs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ public static IHttpClientBuilder AddRefitClient<T>(
2828
return AddRefitClient<T>(services, _ => settings);
2929
}
3030

31+
/// <summary>
32+
/// Adds a Refit client to the DI container
33+
/// </summary>
34+
/// <typeparam name="T">Type of the Refit interface</typeparam>
35+
/// <param name="services">container</param>
36+
/// <param name="serviceKey">An optional key to associate with the specific Refit client instance</param>
37+
/// <param name="settings">Optional. Settings to configure the instance with</param>
38+
/// <returns></returns>
39+
public static IHttpClientBuilder AddKeyedRefitClient<T>(
40+
this IServiceCollection services,
41+
object? serviceKey,
42+
RefitSettings? settings = null
43+
)
44+
where T : class
45+
{
46+
return AddKeyedRefitClient<T>(services, serviceKey, _ => settings);
47+
}
48+
3149
/// <summary>
3250
/// Adds a Refit client to the DI container
3351
/// </summary>
@@ -44,6 +62,24 @@ public static IHttpClientBuilder AddRefitClient(
4462
return AddRefitClient(services, refitInterfaceType, _ => settings);
4563
}
4664

65+
/// <summary>
66+
/// Adds a Refit client to the DI container
67+
/// </summary>
68+
/// <param name="services">container</param>
69+
/// <param name="refitInterfaceType">Type of the Refit interface</param>
70+
/// <param name="serviceKey">An optional key to associate with the specific Refit client instance</param>
71+
/// <param name="settings">Optional. Settings to configure the instance with</param>
72+
/// <returns></returns>
73+
public static IHttpClientBuilder AddKeyedRefitClient(
74+
this IServiceCollection services,
75+
Type refitInterfaceType,
76+
object? serviceKey,
77+
RefitSettings? settings = null
78+
)
79+
{
80+
return AddKeyedRefitClient(services, refitInterfaceType, serviceKey, _ => settings);
81+
}
82+
4783
/// <summary>
4884
/// Adds a Refit client to the DI container
4985
/// </summary>
@@ -91,6 +127,60 @@ public static IHttpClientBuilder AddRefitClient<T>(
91127
);
92128
}
93129

130+
/// <summary>
131+
/// Adds a Refit client to the DI container with a specified service key
132+
/// </summary>
133+
/// <typeparam name="T">Type of the Refit interface</typeparam>
134+
/// <param name="services">container</param>
135+
/// <param name="serviceKey">An optional key to associate with the specific Refit client instance</param>
136+
/// <param name="settingsAction">Optional. Action to configure refit settings. This method is called once and only once, avoid using any scoped dependencies that maybe be disposed automatically.</param>
137+
/// <param name="httpClientName">Optional. Allows users to change the HttpClient name as provided to IServiceCollection.AddHttpClient. Useful for logging scenarios.</param>
138+
/// <returns></returns>
139+
public static IHttpClientBuilder AddKeyedRefitClient<T>(
140+
this IServiceCollection services,
141+
object? serviceKey,
142+
Func<IServiceProvider, RefitSettings?>? settingsAction,
143+
string? httpClientName = null
144+
)
145+
where T : class
146+
{
147+
services.AddKeyedSingleton(serviceKey,
148+
(provider, _) => new SettingsFor<T>(settingsAction?.Invoke(provider)));
149+
services.AddKeyedSingleton(
150+
serviceKey,
151+
(provider, _) =>
152+
RequestBuilder.ForType<T>(
153+
provider.GetRequiredKeyedService<SettingsFor<T>>(serviceKey).Settings
154+
)
155+
);
156+
157+
return services
158+
.AddHttpClient(httpClientName ?? UniqueName.ForType<T>(serviceKey))
159+
.ConfigurePrimaryHttpMessageHandler(serviceProvider =>
160+
{
161+
var settings = serviceProvider.GetRequiredKeyedService<SettingsFor<T>>(serviceKey).Settings;
162+
return
163+
settings?.HttpMessageHandlerFactory?.Invoke()
164+
?? new HttpClientHandler();
165+
})
166+
.ConfigureAdditionalHttpMessageHandlers((handlers, serviceProvider) =>
167+
{
168+
var settings = serviceProvider.GetRequiredKeyedService<SettingsFor<T>>(serviceKey).Settings;
169+
if (settings?.AuthorizationHeaderValueGetter is { } getToken)
170+
{
171+
handlers.Add(new AuthenticatedHttpClientHandler(null, getToken));
172+
}
173+
})
174+
.AddKeyedTypedClient(
175+
serviceKey,
176+
(client, serviceProvider) =>
177+
RestService.For<T>(
178+
client,
179+
serviceProvider.GetKeyedService<IRequestBuilder<T>>(serviceKey)!
180+
)
181+
);
182+
}
183+
94184
/// <summary>
95185
/// Adds a Refit client to the DI container
96186
/// </summary>
@@ -158,6 +248,78 @@ public static IHttpClientBuilder AddRefitClient(
158248
);
159249
}
160250

251+
/// <summary>
252+
/// Adds a Refit client to the DI container with a specified service key
253+
/// </summary>
254+
/// <param name="services">container</param>
255+
/// <param name="refitInterfaceType">Type of the Refit interface</param>
256+
/// <param name="serviceKey">An optional key to associate with the specific Refit client instance</param>
257+
/// <param name="settingsAction">Optional. Action to configure refit settings. This method is called once and only once, avoid using any scoped dependencies that maybe be disposed automatically.</param>
258+
/// <param name="httpClientName">Optional. Allows users to change the HttpClient name as provided to IServiceCollection.AddHttpClient. Useful for logging scenarios.</param>
259+
/// <returns></returns>
260+
public static IHttpClientBuilder AddKeyedRefitClient(
261+
this IServiceCollection services,
262+
Type refitInterfaceType,
263+
object? serviceKey,
264+
Func<IServiceProvider, RefitSettings?>? settingsAction,
265+
string? httpClientName = null
266+
)
267+
{
268+
var settingsType = typeof(SettingsFor<>).MakeGenericType(refitInterfaceType);
269+
var requestBuilderType = typeof(IRequestBuilder<>).MakeGenericType(refitInterfaceType);
270+
services.AddKeyedSingleton(
271+
settingsType,
272+
serviceKey,
273+
(provider, _) =>
274+
Activator.CreateInstance(
275+
typeof(SettingsFor<>).MakeGenericType(refitInterfaceType)!,
276+
settingsAction?.Invoke(provider)
277+
)!
278+
);
279+
services.AddKeyedSingleton(
280+
requestBuilderType,
281+
serviceKey,
282+
(provider, _) =>
283+
RequestBuilderGenericForTypeMethod
284+
.MakeGenericMethod(refitInterfaceType)
285+
.Invoke(
286+
null,
287+
new object?[]
288+
{
289+
((ISettingsFor)provider.GetRequiredKeyedService(settingsType, serviceKey)).Settings
290+
}
291+
)!
292+
);
293+
294+
return services
295+
.AddHttpClient(httpClientName ?? UniqueName.ForType(refitInterfaceType, serviceKey))
296+
.ConfigurePrimaryHttpMessageHandler(serviceProvider =>
297+
{
298+
var settings = (ISettingsFor)serviceProvider.GetRequiredKeyedService(settingsType, serviceKey);
299+
return
300+
settings.Settings?.HttpMessageHandlerFactory?.Invoke()
301+
?? new HttpClientHandler();
302+
})
303+
.ConfigureAdditionalHttpMessageHandlers((handlers, serviceProvider) =>
304+
{
305+
var settings = (ISettingsFor)serviceProvider.GetRequiredKeyedService(settingsType, serviceKey);
306+
if (settings.Settings?.AuthorizationHeaderValueGetter is { } getToken)
307+
{
308+
handlers.Add(new AuthenticatedHttpClientHandler(null, getToken));
309+
}
310+
})
311+
.AddKeyedTypedClient(
312+
refitInterfaceType,
313+
serviceKey,
314+
(client, serviceProvider) =>
315+
RestService.For(
316+
refitInterfaceType,
317+
client,
318+
(IRequestBuilder)serviceProvider.GetRequiredKeyedService(requestBuilderType, serviceKey)
319+
)
320+
);
321+
}
322+
161323
private static readonly MethodInfo RequestBuilderGenericForTypeMethod =
162324
typeof(RequestBuilder)
163325
.GetMethods(BindingFlags.Public | BindingFlags.Static)
@@ -214,5 +376,68 @@ Func<HttpClient, IServiceProvider, object> factory
214376

215377
return builder;
216378
}
379+
380+
static IHttpClientBuilder AddKeyedTypedClient(
381+
this IHttpClientBuilder builder,
382+
Type type,
383+
object? serviceKey,
384+
Func<HttpClient, IServiceProvider, object> factory
385+
)
386+
{
387+
if (builder == null)
388+
{
389+
throw new ArgumentNullException(nameof(builder));
390+
}
391+
392+
if (factory == null)
393+
{
394+
throw new ArgumentNullException(nameof(factory));
395+
}
396+
397+
builder.Services.AddKeyedTransient(
398+
type,
399+
serviceKey,
400+
(s, _) =>
401+
{
402+
var httpClientFactory = s.GetRequiredService<IHttpClientFactory>();
403+
var httpClient = httpClientFactory.CreateClient(builder.Name);
404+
405+
return factory(httpClient, s);
406+
}
407+
);
408+
409+
return builder;
410+
}
411+
412+
static IHttpClientBuilder AddKeyedTypedClient<T>(
413+
this IHttpClientBuilder builder,
414+
object? serviceKey,
415+
Func<HttpClient, IServiceProvider, T> factory
416+
)
417+
where T : class
418+
{
419+
if (builder == null)
420+
{
421+
throw new ArgumentNullException(nameof(builder));
422+
}
423+
424+
if (factory == null)
425+
{
426+
throw new ArgumentNullException(nameof(factory));
427+
}
428+
429+
builder.Services.AddKeyedTransient(
430+
serviceKey,
431+
(s, _) =>
432+
{
433+
var httpClientFactory = s.GetRequiredService<IHttpClientFactory>();
434+
var httpClient = httpClientFactory.CreateClient(builder.Name);
435+
436+
return factory(httpClient, s);
437+
}
438+
);
439+
440+
return builder;
441+
}
217442
}
218443
}

Refit.Tests/AuthenticatedClientHandlerTests.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ public void DefaultHandlerIsHttpClientHandler()
7373
Assert.IsType<HttpClientHandler>(handler.InnerHandler);
7474
}
7575

76+
[Fact]
77+
public void DefaultHandlerIsNull()
78+
{
79+
var handler = new AuthenticatedHttpClientHandler(null, ((_, _) => Task.FromResult(string.Empty)));
80+
81+
Assert.Null(handler.InnerHandler);
82+
}
83+
7684
[Fact]
7785
public void NullTokenGetterThrows()
7886
{

Refit.Tests/HttpClientFactoryExtensionsTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ public void GenericHttpClientsAreAssignedUniqueNames()
2929
Assert.NotEqual(userClientName, roleClientName);
3030
}
3131

32+
[Fact]
33+
public void HttpClientServicesAreDifferentThanKeyedServices()
34+
{
35+
var serviceCollection = new ServiceCollection();
36+
serviceCollection.AddRefitClient<IFooWithOtherAttribute>();
37+
serviceCollection.AddKeyedRefitClient<IFooWithOtherAttribute>("keyed");
38+
39+
var serviceProvider = serviceCollection.BuildServiceProvider();
40+
var nonKeyedService = serviceProvider.GetService<IFooWithOtherAttribute>();
41+
var keyedService = serviceProvider.GetKeyedService<IFooWithOtherAttribute>("keyed");
42+
43+
Assert.NotNull(nonKeyedService);
44+
Assert.NotNull(keyedService);
45+
Assert.NotSame(nonKeyedService, keyedService);
46+
47+
var nonKeyedSettings = serviceProvider.GetService<SettingsFor<IFooWithOtherAttribute>>();
48+
var keyedSettings = serviceProvider.GetKeyedService<SettingsFor<IFooWithOtherAttribute>>("keyed");
49+
Assert.NotSame(nonKeyedSettings, keyedSettings);
50+
51+
var nonKeyedRequestBuilder = serviceProvider.GetService<IRequestBuilder<IFooWithOtherAttribute>>();
52+
var keyedRequestBuilder = serviceProvider.GetKeyedService<IRequestBuilder<IFooWithOtherAttribute>>("keyed");
53+
Assert.NotSame(nonKeyedRequestBuilder, keyedRequestBuilder);
54+
}
55+
3256
[Fact]
3357
public void HttpClientServicesAreAddedCorrectlyGivenGenericArgument()
3458
{

Refit/AuthenticatedHttpClientHandler.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ class AuthenticatedHttpClientHandler : DelegatingHandler
77
{
88
readonly Func<HttpRequestMessage, CancellationToken, Task<string>> getToken;
99

10+
/// <summary>
11+
/// Initializes a new instance of the <see cref="AuthenticatedHttpClientHandler"/> class.
12+
/// </summary>
13+
/// <param name="getToken">The function to get the authentication token.</param>
14+
/// <param name="innerHandler">The optional inner handler.</param>
15+
/// <exception cref="ArgumentNullException"><paramref name="getToken"/> must not be null.</exception>
16+
/// <remarks>
17+
/// Warning: This constructor sets the <see cref="DelegatingHandler.InnerHandler"/> to an instance
18+
/// of <see cref="HttpClientHandler"/>, when <paramref name="innerHandler"/> is <c>null</c>. This is
19+
/// a behavior which is incompatible with the <code>IHttpClientBuilder</code>.
20+
/// </remarks>
1021
public AuthenticatedHttpClientHandler(
1122
Func<HttpRequestMessage, CancellationToken, Task<string>> getToken,
1223
HttpMessageHandler? innerHandler = null
@@ -16,6 +27,28 @@ public AuthenticatedHttpClientHandler(
1627
this.getToken = getToken ?? throw new ArgumentNullException(nameof(getToken));
1728
}
1829

30+
/// <summary>
31+
/// Initializes a new instance of the <see cref="AuthenticatedHttpClientHandler"/> class.
32+
/// </summary>
33+
/// <param name="innerHandler">The optional inner handler.</param>
34+
/// <param name="getToken">The function to get the authentication token.</param>
35+
/// <exception cref="ArgumentNullException"><paramref name="getToken"/> must not be null.</exception>
36+
/// <remarks>
37+
/// This function doesn't set the <see cref="DelegatingHandler.InnerHandler"/> automatically to an
38+
/// instance of <see cref="HttpClientHandler"/> when <paramref name="innerHandler"/> is null,
39+
/// which is different from the old (legacy) constructor, and compliant with the behavior expected
40+
/// by the <code>IHttpClientBuilder</code>.
41+
/// </remarks>
42+
public AuthenticatedHttpClientHandler(
43+
HttpMessageHandler? innerHandler,
44+
Func<HttpRequestMessage, CancellationToken, Task<string>> getToken
45+
)
46+
{
47+
this.getToken = getToken ?? throw new ArgumentNullException(nameof(getToken));
48+
if (innerHandler != null)
49+
InnerHandler = innerHandler;
50+
}
51+
1952
protected override async Task<HttpResponseMessage> SendAsync(
2053
HttpRequestMessage request,
2154
CancellationToken cancellationToken

Refit/UniqueName.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ public static string ForType<T>()
77
return ForType(typeof(T));
88
}
99

10+
public static string ForType<T>(object? serviceKey)
11+
{
12+
return ForType(typeof(T), serviceKey);
13+
}
14+
15+
public static string ForType(Type refitInterfaceType, object? serviceKey)
16+
{
17+
return ForType(refitInterfaceType) + GetServiceKeySuffix(serviceKey);
18+
}
19+
1020
public static string ForType(Type refitInterfaceType)
1121
{
1222
var interfaceTypeName = refitInterfaceType.FullName!;
@@ -52,5 +62,15 @@ public static string ForType(Type refitInterfaceType)
5262

5363
return assmQualified;
5464
}
65+
66+
/// <summary>
67+
/// Returns the suffix for the service key to be added to the unique name for a given type.
68+
/// </summary>
69+
/// <param name="serviceKey">The service key to create the suffix from.</param>
70+
/// <returns>The suffix to be added to the unique name of a given type.</returns>
71+
static string GetServiceKeySuffix(object? serviceKey)
72+
{
73+
return serviceKey is null or "" ? string.Empty : $", ServiceKey={serviceKey}";
74+
}
5575
}
5676
}

0 commit comments

Comments
 (0)