Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to register typed httpClient service with autofac?

I'm creating MVC web application which calls an api using .net core 2.2 using separate HttpClients to call each controller (same api).

Ex:

  • For user controller actions : UserService (httpclient)
  • For post controller actions : PostService (httpclient)

In startup.cs I use DI as:

services.AddHttpClient<IUserService, UserService>();
services.AddHttpClient<IPostService, PostService>();

In my handler :

public class CommandHandler : IRequestHandler<Command, BaseResponse>
{
    private readonly IUserService _userService;

    public CommandHandler(IUserService userService)
    {
        _userService = userService;
    }

    public Task<BaseResponse> Handle(Command request, CancellationToken cancellationToken)
    {
        throw new System.NotImplementedException();
    }
}

But when invoking command handler I get this error:

None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'xxx.Application.Services.Users.UserService' can be invoked with the available services and parameters: Cannot resolve parameter 'System.Net.Http.HttpClient httpClient' of constructor 'Void .ctor(System.Net.Http.HttpClient, xxx.Application.Configurations.IApplicationConfigurations, Microsoft.Extensions.Logging.ILogger`1[xxx.Application.Services.Users.UserService])'.

But I've registered services in autofac module:

public class ServiceModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterAssemblyTypes(typeof(ServiceModule).Assembly)
                .Where(t => t.Namespace.StartsWith("xxx.Application.Services"))
                .AsImplementedInterfaces().InstancePerLifetimeScope();
    }
}

Here is my UserService class constructor:

public UserService (HttpClient httpClient, IApplicationConfigurations applicationConfig, ILogger<UserService> logger)
{
    _httpClient = httpClient;
    _applicationConfig = applicationConfig;
    _logger = logger;

    _remoteServiceBaseUrl = $"{_applicationConfig.WebApiBaseUrl}";
}

I have two questions:

  1. What does the above error mean?
  2. Is it good practice to use separate httpclients for different controllers in api?
like image 632
Roshan Avatar asked Nov 10 '19 17:11

Roshan


3 Answers

By doing

services.AddHttpClient<IUserService, UserService>();  

You will configure the native .net core dependency injection to inject HttpClient to UserService when a IUserService is requested.

Then you do

builder.RegisterAssemblyTypes(typeof(ServiceModule).Assembly)
       .Where(t => t.Namespace.StartsWith("xxx.Application.Services"))
       .AsImplementedInterfaces().InstancePerLifetimeScope();

which will erase the native dependency injection configuration for IUserService. The IUserService is now registered with UserService without any HttpClient in mind.

The simplest way to add HttpClient would be to register it like this :

builder.Register(c => new HttpClient())
       .As<HttpClient>();

or

services.AddHttpClient(); // register the .net core IHttpClientFactory 
builder.Register(c => c.Resolve<IHttpClientFactory>().CreateClient())
       .As<HttpClient>(); 

If you want to configure your httpclient for a specific service you can create an autofac module which add parameters like this :

public class HttpClientModule<TService> : Module
{
    public HttpClientModule(Action<HttpClient> clientConfigurator)
    {
        this._clientConfigurator = clientConfigurator;
    }

    private readonly Action<HttpClient> _clientConfigurator;

    protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
    {
        base.AttachToComponentRegistration(componentRegistry, registration);

        if (registration.Activator.LimitType == typeof(TService))
        {
            registration.Preparing += (sender, e) =>
            {
                e.Parameters = e.Parameters.Union(
                  new[]
                  {
                    new ResolvedParameter(
                        (p, i) => p.ParameterType == typeof(HttpClient),
                        (p, i) => {
                            HttpClient client = i.Resolve<IHttpClientFactory>().CreateClient();
                            this._clientConfigurator(client);
                            return client;
                        }
                    )
                  });
            };
        }
    }
}

Then

builder.RegisterModule(new HttpClientModule<UserService>(client =>
{
    client.BaseAddress = new Uri("https://api.XXX.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.XXX.v3+json");
    client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-XXX");
}));
like image 178
Cyril Durand Avatar answered Nov 20 '22 05:11

Cyril Durand


Cyril's implementation of using an Autofac module works wonderfully, but unfortunately is not compatible with Autofac 6.0+.

In order to configure an HttpClient in Autofac 6.0+ for a specific service, an Autofac middleware needs to be implemented:

public class HttpClientMiddleware<TService> : IResolveMiddleware
{
    private readonly Action<HttpClient> _clientConfigurator;

    public HttpClientMiddleware(Action<HttpClient> clientConfigurator)
    {
        _clientConfigurator = clientConfigurator;
    }

    public PipelinePhase Phase => PipelinePhase.ParameterSelection;

    public void Execute(ResolveRequestContext context, Action<ResolveRequestContext> next)
    {
        if (context.Registration.Activator.LimitType == typeof(TService))
        {
            context.ChangeParameters(context.Parameters.Union(
                new[]
                {
                    new ResolvedParameter(
                        (p, _) => p.ParameterType == typeof(HttpClient),
                        (_, i) => {
                            var client = i.Resolve<IHttpClientFactory>().CreateClient();
                            _clientConfigurator(client);
                            return client;
                        }
                    )
                }));
        }

        next(context);
    }
}

Then the service can be registered, utilizing the middleware:

builder.RegisterType<UserService>()
    .As<IUserService>()
    .ConfigurePipeline(p =>
    {
        p.Use(new HttpClientMiddleware<UserService>(client =>
        {
            client.BaseAddress = new Uri("https://api.XXX.com/");
            client.DefaultRequestHeaders.Add("Accept", "application/vnd.XXX.v3+json");
            client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-XXX");
        }));
    });
like image 40
bmeredith Avatar answered Nov 20 '22 04:11

bmeredith


You can register any type with httpclient as follows extension method.

public static ContainerBuilder RegisterWithHttpClient<TInterface, TClass>(this ContainerBuilder builder, Action<IComponentContext, HttpClient> config)
        where TClass: class
    {
        builder
            .RegisterType<TClass>()
            .AsSelf()
            .As<TInterface>()
            .WithParameter(new ResolvedParameter(
                (info, context) => info.ParameterType.IsAssignableFrom(typeof(HttpClient)),
                (info, context) =>
                {
                    var httpClient = context.Resolve<IHttpClientFactory>().CreateClient();
                    config.Invoke(context, httpClient);

                    return httpClient;
                }
            ))
            .InstancePerLifetimeScope();

        return builder;
    }

and register your type.

    //in startup.cs or autofac module.
    public void ConfigureContainer(ContainerBuilder container)
    {
        container.RegisterWithHttpClient<IEmailSender, MyEmailSender>((context, client) =>
        {
            var settings = context.Resolve<IOptionsSnapshot<EmailSenderSettings>>().Value;
            client.BaseAddress = new Uri($"{settings.ApiBaseUrl.TrimEnd('/')}/");
            client.Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds);
        });
    }
like image 36
htekir Avatar answered Nov 20 '22 06:11

htekir