From 392310a74cac54cb7b99467b6dcb55e29403fcff Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Fri, 24 Jul 2026 14:59:17 +1200 Subject: [PATCH] refactor: tidy the extension + multi-target (netstandard2.0;net8.0) 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 --- .../Extensions/HttpClientExtensions.cs | 135 +++++++----------- .../Scrutor.Extensions.HttpClient.csproj | 10 ++ 2 files changed, 58 insertions(+), 87 deletions(-) diff --git a/Scrutor.Extensions.HttpClient/Extensions/HttpClientExtensions.cs b/Scrutor.Extensions.HttpClient/Extensions/HttpClientExtensions.cs index d1aec71..073f9f1 100644 --- a/Scrutor.Extensions.HttpClient/Extensions/HttpClientExtensions.cs +++ b/Scrutor.Extensions.HttpClient/Extensions/HttpClientExtensions.cs @@ -1,116 +1,77 @@ -using System; -using System.Collections.Generic; +using System; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Reflection; using Scrutor; namespace Microsoft.Extensions.DependencyInjection; /// -/// Add DI capabilities for Scrutor to work with HttpClient +/// Scrutor selector extensions that register each scanned class as a typed +/// (through ) +/// instead of a plain transient/scoped/singleton service. /// -/// https://github.com/khellang/Scrutor/issues/180 +/// Bridges Scrutor assembly scanning with Microsoft.Extensions.Http. +/// See https://github.com/khellang/Scrutor/issues/180. public static class HttpClientExtensions { /// - /// Add as Named + /// Registers each scanned service as a typed bound to + /// the named client (so they share that client's configuration). /// - /// - /// - /// + /// The Scrutor service-type selector. + /// The name of a client registered via AddHttpClient(name, ...). public static IServiceTypeSelector AsHttpClient(this IServiceTypeSelector selector, string name = "") - { - var strategy = new NamedHttpClientRegistrationStrategy(name); - return selector.UsingRegistrationStrategy(strategy); - } + => selector.UsingRegistrationStrategy(new HttpClientRegistrationStrategy(name)); /// - /// Add as + /// Registers each scanned service as a typed , each with + /// its own default client named after the service type. /// - /// - /// + /// The Scrutor service-type selector. public static IServiceTypeSelector AsHttpClient(this IServiceTypeSelector selector) - { - return selector.UsingRegistrationStrategy(HttpClientRegistrationStrategy.Instance); - } + => selector.UsingRegistrationStrategy(new HttpClientRegistrationStrategy(name: null)); /// - /// for Named + /// A Scrutor that registers each scanned + /// (service, implementation) pair as a typed HttpClient. 's + /// AddHttpClient<TClient,TImplementation> has no non-generic overload, so the closed + /// generic method is resolved once and invoked per scanned type. /// - private class NamedHttpClientRegistrationStrategy(string httpClientName) : RegistrationStrategy + private sealed class HttpClientRegistrationStrategy(string? name) : RegistrationStrategy { - public string HttpClientName { get; } = httpClientName; - public static RegistrationStrategy Instance { get; private set; } - public static RegistrationStrategy GetInstance(string httpClientName) - { - Instance ??= new NamedHttpClientRegistrationStrategy(httpClientName); - return Instance; - } + // Resolved lazily and cached: the AddHttpClient<,> generic definition matching whether a + // client name was supplied. Named → (IServiceCollection, string); unnamed → (IServiceCollection). + private static readonly MethodInfo NamedAddHttpClient = ResolveAddHttpClient(named: true); + private static readonly MethodInfo UnnamedAddHttpClient = ResolveAddHttpClient(named: false); public override void Apply(IServiceCollection services, ServiceDescriptor descriptor) { - Type TInterface = descriptor.ServiceType; - Type TClass = descriptor.ImplementationType!; - Type TImplementingClass = typeof(HttpClientFactoryServiceCollectionExtensions); - - - /* Get all methods named "AddHttpClient" */ - var methods = TImplementingClass.GetMethods() - .Where(m => m.Name == "AddHttpClient" && m.IsGenericMethodDefinition) - .ToArray(); + var serviceType = descriptor.ServiceType; + var implementationType = descriptor.ImplementationType + ?? throw new InvalidOperationException( + $"AsHttpClient() requires a concrete implementation type, but the descriptor for " + + $"'{serviceType}' has none. Pair it with a class-based Scrutor selector such as " + + $"AddClasses(...).AsMatchingInterface() / .AsSelf()."); - /* Then get the one that matches the amount of generic arguments we want to provide, - * in our case 2: Interface + Implementation AddHttpClient - * Can be modified to add more parameters, but minimum is two: AddHttpClient(services, name) - */ - var method = methods.FirstOrDefault(m => - { - var parameters = m.GetParameters(); - var genericArguments = m.GetGenericArguments(); - return - parameters.Length == 2 && genericArguments.Length == 2 && - parameters[0].ParameterType == typeof(IServiceCollection) && parameters[1].ParameterType == typeof(string) - ; - }); + var closed = (name is null ? UnnamedAddHttpClient : NamedAddHttpClient) + .MakeGenericMethod(serviceType, implementationType); - var genericMethod = method.MakeGenericMethod(TInterface, TClass); - genericMethod.Invoke(services, [services, HttpClientName]); + var arguments = name is null ? new object[] { services } : new object[] { services, name }; + closed.Invoke(obj: null, arguments); } - } - /// - /// for - /// - private class HttpClientRegistrationStrategy : RegistrationStrategy - { - public static readonly RegistrationStrategy Instance = new HttpClientRegistrationStrategy(); - - public override void Apply(IServiceCollection services, ServiceDescriptor descriptor) - { - Type TInterface = descriptor.ServiceType; - Type TClass = descriptor.ImplementationType!; - Type TImplementingClass = typeof(HttpClientFactoryServiceCollectionExtensions); + private static MethodInfo ResolveAddHttpClient(bool named) => + typeof(HttpClientFactoryServiceCollectionExtensions).GetMethods() + .SingleOrDefault(method => + method is { Name: nameof(HttpClientFactoryServiceCollectionExtensions.AddHttpClient), IsGenericMethodDefinition: true } + && method.GetGenericArguments().Length == 2 + && HasShape(method.GetParameters(), named)) + ?? throw new InvalidOperationException( + $"Could not locate the AddHttpClient({(named ? "IServiceCollection, string" : "IServiceCollection")}) " + + $"overload on {nameof(HttpClientFactoryServiceCollectionExtensions)}. The Microsoft.Extensions.Http API may have changed."); - - /* Get all methods named "AddHttpClient" */ - var methods = TImplementingClass.GetMethods() - .Where(m => m.Name == "AddHttpClient" && m.IsGenericMethodDefinition) - .ToArray(); - - /* Then get the one that matches the amount of generic arguments we want to provide, - * in our case 2: Interface + Implementation AddHttpClient - * Can be modified to add more parameters, but minimum is one AddHttpClient(services) - */ - var method = methods.FirstOrDefault(m => - { - var parameters = m.GetParameters(); - var genericArguments = m.GetGenericArguments(); - return parameters.Length == 1 && genericArguments.Length == 2 && parameters[0].ParameterType == typeof(IServiceCollection); - }); - - var genericMethod = method.MakeGenericMethod(TInterface, TClass); - genericMethod.Invoke(services, [services]); - } + private static bool HasShape(ParameterInfo[] parameters, bool named) => named + ? parameters.Length == 2 && parameters[0].ParameterType == typeof(IServiceCollection) && parameters[1].ParameterType == typeof(string) + : parameters.Length == 1 && parameters[0].ParameterType == typeof(IServiceCollection); } -} \ No newline at end of file +} diff --git a/Scrutor.Extensions.HttpClient/Scrutor.Extensions.HttpClient.csproj b/Scrutor.Extensions.HttpClient/Scrutor.Extensions.HttpClient.csproj index 25aad83..109eef2 100644 --- a/Scrutor.Extensions.HttpClient/Scrutor.Extensions.HttpClient.csproj +++ b/Scrutor.Extensions.HttpClient/Scrutor.Extensions.HttpClient.csproj @@ -1,5 +1,15 @@ + + + netstandard2.0;net8.0 + + latest + + Scrutor HttpClient Extension Chrison Simtian