Skip to content

Commit dc34f4c

Browse files
refactor: tidy the extension + multi-target (netstandard2.0;net8.0) (#5)
Tidy / bug fixes (clears all CS8618/CS8602 warnings): - Consolidate the two near-identical registration strategies into one that takes an optional client name; remove the dead, never-assigned Instance/GetInstance singleton (the CS8618 source). - Null-check the reflected AddHttpClient<,> lookup and the descriptor's implementation type, throwing clear InvalidOperationExceptions instead of an opaque NullReferenceException if the MEH API shape ever changes or the selector is misused. Cache the resolved generic method definitions. - Drop unused usings; XML docs on the public overloads. Multi-target: - netstandard2.0;net8.0 (was net8.0 only) for broad consumer reach; both Scrutor 7 and Microsoft.Extensions.Http support these. LangVersion=latest so modern C# compiles on netstandard2.0. Public API surface unchanged (PublicApiGenerator baseline still green); all 7 specs pass; package now ships lib/net8.0 + lib/netstandard2.0. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c02d2ac commit dc34f4c

2 files changed

Lines changed: 58 additions & 87 deletions

File tree

Lines changed: 48 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,77 @@
1-
using System;
2-
using System.Collections.Generic;
1+
using System;
32
using System.Linq;
4-
using System.Text;
5-
using System.Threading.Tasks;
3+
using System.Reflection;
64
using Scrutor;
75

86
namespace 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>
1415
public 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&lt;TClient,TImplementation&gt;</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+
}

‎Scrutor.Extensions.HttpClient/Scrutor.Extensions.HttpClient.csproj‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3+
<PropertyGroup Label="Target Frameworks">
4+
<!-- netstandard2.0 for max reach (net462+, .NET Core 2+, Mono, …); net8.0 for modern
5+
consumers. Overrides the single TFM in the root Directory.Build.props. Both Scrutor 7
6+
and Microsoft.Extensions.Http support these targets. LangVersion lets us use modern
7+
C# (primary constructors, list patterns) even on the netstandard2.0 target. -->
8+
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
9+
<TargetFramework></TargetFramework>
10+
<LangVersion>latest</LangVersion>
11+
</PropertyGroup>
12+
313
<PropertyGroup Label="Package">
414
<Title>Scrutor HttpClient Extension</Title>
515
<Authors>Chrison Simtian</Authors>

0 commit comments

Comments
 (0)