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