1- using System ;
2- using System . Collections . Generic ;
1+ using System ;
32using System . Linq ;
4- using System . Text ;
5- using System . Threading . Tasks ;
3+ using System . Reflection ;
64using Scrutor ;
75
86namespace Microsoft . Extensions . DependencyInjection ;
97
108/// <summary>
11- /// Add DI capabilities for Scrutor to work with HttpClient
9+ /// Scrutor selector extensions that register each scanned class as a <em>typed</em>
10+ /// <see cref="System.Net.Http.HttpClient"/> (through <see cref="IHttpClientFactory"/>)
11+ /// instead of a plain transient/scoped/singleton service.
1212/// </summary>
13- /// <remarks>https://github.com/khellang/Scrutor/issues/180</remarks>
13+ /// <remarks>Bridges Scrutor assembly scanning with <c>Microsoft.Extensions.Http</c>.
14+ /// See https://github.com/khellang/Scrutor/issues/180.</remarks>
1415public static class HttpClientExtensions
1516{
1617 /// <summary>
17- /// Add as Named <see cref="HttpClient"/>
18+ /// Registers each scanned service as a typed <see cref="System.Net.Http.HttpClient"/> bound to
19+ /// the named client <paramref name="name"/> (so they share that client's configuration).
1820 /// </summary>
19- /// <param name="selector"></param>
20- /// <param name="name"></param>
21- /// <returns></returns>
21+ /// <param name="selector">The Scrutor service-type selector.</param>
22+ /// <param name="name">The name of a client registered via <c>AddHttpClient(name, ...)</c>.</param>
2223 public static IServiceTypeSelector AsHttpClient ( this IServiceTypeSelector selector , string name = "" )
23- {
24- var strategy = new NamedHttpClientRegistrationStrategy ( name ) ;
25- return selector . UsingRegistrationStrategy ( strategy ) ;
26- }
24+ => selector . UsingRegistrationStrategy ( new HttpClientRegistrationStrategy ( name ) ) ;
2725
2826 /// <summary>
29- /// Add as <see cref="HttpClient"/>
27+ /// Registers each scanned service as a typed <see cref="System.Net.Http.HttpClient"/>, each with
28+ /// its own default client named after the service type.
3029 /// </summary>
31- /// <param name="selector"></param>
32- /// <returns></returns>
30+ /// <param name="selector">The Scrutor service-type selector.</param>
3331 public static IServiceTypeSelector AsHttpClient ( this IServiceTypeSelector selector )
34- {
35- return selector . UsingRegistrationStrategy ( HttpClientRegistrationStrategy . Instance ) ;
36- }
32+ => selector . UsingRegistrationStrategy ( new HttpClientRegistrationStrategy ( name : null ) ) ;
3733
3834 /// <summary>
39- /// <see cref="RegistrationStrategy"/> for Named <see cref="HttpClient"/>
35+ /// A Scrutor <see cref="RegistrationStrategy"/> that registers each scanned
36+ /// (service, implementation) pair as a typed HttpClient. <see cref="System.Net.Http.HttpClient"/>'s
37+ /// <c>AddHttpClient<TClient,TImplementation></c> has no non-generic overload, so the closed
38+ /// generic method is resolved once and invoked per scanned type.
4039 /// </summary>
41- private class NamedHttpClientRegistrationStrategy ( string httpClientName ) : RegistrationStrategy
40+ private sealed class HttpClientRegistrationStrategy ( string ? name ) : RegistrationStrategy
4241 {
43- public string HttpClientName { get ; } = httpClientName ;
44- public static RegistrationStrategy Instance { get ; private set ; }
45- public static RegistrationStrategy GetInstance ( string httpClientName )
46- {
47- Instance ??= new NamedHttpClientRegistrationStrategy ( httpClientName ) ;
48- return Instance ;
49- }
42+ // Resolved lazily and cached: the AddHttpClient<,> generic definition matching whether a
43+ // client name was supplied. Named → (IServiceCollection, string); unnamed → (IServiceCollection).
44+ private static readonly MethodInfo NamedAddHttpClient = ResolveAddHttpClient ( named : true ) ;
45+ private static readonly MethodInfo UnnamedAddHttpClient = ResolveAddHttpClient ( named : false ) ;
5046
5147 public override void Apply ( IServiceCollection services , ServiceDescriptor descriptor )
5248 {
53- Type TInterface = descriptor . ServiceType ;
54- Type TClass = descriptor . ImplementationType ! ;
55- Type TImplementingClass = typeof ( HttpClientFactoryServiceCollectionExtensions ) ;
56-
57-
58- /* Get all methods named "AddHttpClient" */
59- var methods = TImplementingClass . GetMethods ( )
60- . Where ( m => m . Name == "AddHttpClient" && m . IsGenericMethodDefinition )
61- . ToArray ( ) ;
49+ var serviceType = descriptor . ServiceType ;
50+ var implementationType = descriptor . ImplementationType
51+ ?? throw new InvalidOperationException (
52+ $ "AsHttpClient() requires a concrete implementation type, but the descriptor for " +
53+ $ "'{ serviceType } ' has none. Pair it with a class-based Scrutor selector such as " +
54+ $ "AddClasses(...).AsMatchingInterface() / .AsSelf().") ;
6255
63- /* Then get the one that matches the amount of generic arguments we want to provide,
64- * in our case 2: Interface + Implementation AddHttpClient<TInterface,TImplementation>
65- * Can be modified to add more parameters, but minimum is two: AddHttpClient(services, name)
66- */
67- var method = methods . FirstOrDefault ( m =>
68- {
69- var parameters = m . GetParameters ( ) ;
70- var genericArguments = m . GetGenericArguments ( ) ;
71- return
72- parameters . Length == 2 && genericArguments . Length == 2 &&
73- parameters [ 0 ] . ParameterType == typeof ( IServiceCollection ) && parameters [ 1 ] . ParameterType == typeof ( string )
74- ;
75- } ) ;
56+ var closed = ( name is null ? UnnamedAddHttpClient : NamedAddHttpClient )
57+ . MakeGenericMethod ( serviceType , implementationType ) ;
7658
77- var genericMethod = method . MakeGenericMethod ( TInterface , TClass ) ;
78- genericMethod . Invoke ( services , [ services , HttpClientName ] ) ;
59+ var arguments = name is null ? new object [ ] { services } : new object [ ] { services , name } ;
60+ closed . Invoke ( obj : null , arguments ) ;
7961 }
80- }
8162
82- /// <summary>
83- /// <see cref="RegistrationStrategy"/> for <see cref="HttpClient"/>
84- /// </summary>
85- private class HttpClientRegistrationStrategy : RegistrationStrategy
86- {
87- public static readonly RegistrationStrategy Instance = new HttpClientRegistrationStrategy ( ) ;
88-
89- public override void Apply ( IServiceCollection services , ServiceDescriptor descriptor )
90- {
91- Type TInterface = descriptor . ServiceType ;
92- Type TClass = descriptor . ImplementationType ! ;
93- Type TImplementingClass = typeof ( HttpClientFactoryServiceCollectionExtensions ) ;
63+ private static MethodInfo ResolveAddHttpClient ( bool named ) =>
64+ typeof ( HttpClientFactoryServiceCollectionExtensions ) . GetMethods ( )
65+ . SingleOrDefault ( method =>
66+ method is { Name : nameof ( HttpClientFactoryServiceCollectionExtensions . AddHttpClient ) , IsGenericMethodDefinition : true }
67+ && method . GetGenericArguments ( ) . Length == 2
68+ && HasShape ( method . GetParameters ( ) , named ) )
69+ ?? throw new InvalidOperationException (
70+ $ "Could not locate the AddHttpClient<TClient,TImplementation>({ ( named ? "IServiceCollection, string" : "IServiceCollection" ) } ) " +
71+ $ "overload on { nameof ( HttpClientFactoryServiceCollectionExtensions ) } . The Microsoft.Extensions.Http API may have changed.") ;
9472
95-
96- /* Get all methods named "AddHttpClient" */
97- var methods = TImplementingClass . GetMethods ( )
98- . Where ( m => m . Name == "AddHttpClient" && m . IsGenericMethodDefinition )
99- . ToArray ( ) ;
100-
101- /* Then get the one that matches the amount of generic arguments we want to provide,
102- * in our case 2: Interface + Implementation AddHttpClient<TInterface,TImplementation>
103- * Can be modified to add more parameters, but minimum is one AddHttpClient(services)
104- */
105- var method = methods . FirstOrDefault ( m =>
106- {
107- var parameters = m . GetParameters ( ) ;
108- var genericArguments = m . GetGenericArguments ( ) ;
109- return parameters . Length == 1 && genericArguments . Length == 2 && parameters [ 0 ] . ParameterType == typeof ( IServiceCollection ) ;
110- } ) ;
111-
112- var genericMethod = method . MakeGenericMethod ( TInterface , TClass ) ;
113- genericMethod . Invoke ( services , [ services ] ) ;
114- }
73+ private static bool HasShape ( ParameterInfo [ ] parameters , bool named ) => named
74+ ? parameters . Length == 2 && parameters [ 0 ] . ParameterType == typeof ( IServiceCollection ) && parameters [ 1 ] . ParameterType == typeof ( string )
75+ : parameters . Length == 1 && parameters [ 0 ] . ParameterType == typeof ( IServiceCollection ) ;
11576 }
116- }
77+ }
0 commit comments