I'm using dependency injection with HttpClient and I'm trying to figure out how to set a baseurl, but can't seem to figure it out.
I'm doing it this way:
public static async Task<HttpResponseMessage> PostUser(User user) {
var services = new ServiceCollection();
services.UseServices();
var serviceProvider = services.BuildServiceProvider();
var service = serviceProvider.GetRequiredService<IUserService>();
return await service.PostUser(user);
}
class UserService : IUserService
{
private readonly HttpClient _httpClient;
public UserService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<HttpResponseMessage> PostUser(User user)
{
HttpResponseMessage response = await _httpClient.PostAsJsonAsync(BASEURL, user);
return response;
}
}
I register in this way:
public static class Bootstrapper
{
public static void UseServices(this IServiceCollection services)
{
services.AddHttpClient<IUserService, UserService>();
}
}
So I want to use the BASEURL in the above, but how can I pass it with the httpClient?
This should work:
public static class Bootstrapper
{
public static void UseServices(this IServiceCollection services)
{
services.AddHttpClient<IUserServicee, UserService>(
client => client.BaseAddress = new Uri("YOUR_BASE_ADDRESS"));
}
}
Method overload takes Action<HttpClient> as an argument so it's void and you can mutate your HttpClient instance in the way you want.
Related
I have a custom HttpMessageHandler implementation:
public class MyHandler : DelegatingHandler
{
private readonly HttpClient _httpClient;
private readonly MyHandlerOptions _config;
public MyHandler(
HttpClient httpClient,
IOptions<MyHandlerOptions> options)
{
_httpClient = httpClient;
_config = options.Value;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = await GetAccessToken()
return await base.SendAsync(request, cancellationToken);
}
private async Task<string> GetAccessToken()
{
//some logic to get access token using _httpClient and _config
}
}
It requires confiuration object MyHandlerOptions. Its form is not so important here. It basically contains clientId, clientSecret, etc. that are needed for the handler to know how to get the access token.
I have a few services (typed http clients) that need to use MyHandler:
//registration of MyHandler itself
builder.Services.AddHttpClient<MyHandler>();
//configuration of MyHandler
builder.Services.AddOptions<MyHandlerOptions>()
.Configure<IConfiguration>((config, configuration) =>
{
configuration.GetSection("MyHandlerOptions").Bind(config);
});
//Services that need to use MyHandler:
services.AddHttpClient<Service1>()
.AddHttpMessageHandler<MyHandler>();
services.AddHttpClient<Service2>()
.AddHttpMessageHandler<MyHandler>();
services.AddHttpClient<Service3>()
.AddHttpMessageHandler<MyHandler>();
The problem is that the MyHandlerOptions instance that I registered is valid only when used with Service1. However, Service2 and Service3 require other configuration (different clientId, clientSecret, etc.). How can I achieve it?
The possible solution that comes to my mind:
Create a new service:
public class AccessTokenGetter
{
Task<string> GetAccessToken(AccessTokenConfig config)
{
//get the access token...
}
}
Create separate HttpMessageHandlers for each case where configuration is different:
public class MyHandler1 : DelegatingHandler
{
private readonly MyHandler1Options _config;
private readonly AccessTokenGetter _accessTokenGetter;
public MyHandler(AccessTokenGetter accessTokenGetter, IOptions<MyHandlerOptions1> options)
{
_accessTokenGetter = accessTokenGetter;
_config = options.Value;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//somehow convert _config to AccessTokenConfig
request.Headers.Authorization = await _accessTokenGetter.GetAccessToken(_config)
return await base.SendAsync(request, cancellationToken);
}
}
public class MyHandler2 : DelegatingHandler
{
//same implementation as MyHandler1, just use MyHandler2Options instead
}
Register my services:
//configurations
builder.Services.AddOptions<MyHandler1Options>()
.Configure<IConfiguration>((config, configuration) =>
{
configuration.GetSection("MyHandler1Options").Bind(config);
});
builder.Services.AddOptions<MyHandler2Options>()
.Configure<IConfiguration>((config, configuration) =>
{
configuration.GetSection("MyHandler2Options").Bind(config);
});
//AccessTokenGetter
services.AddHttpClient<AccessTokenGetter>()
//Services that need to use MyHandlers:
services.AddHttpClient<Service1>()
.AddHttpMessageHandler<MyHandler1>();
services.AddHttpClient<Service2>()
.AddHttpMessageHandler<MyHandler2>();
services.AddHttpClient<Service3>()
.AddHttpMessageHandler<MyHandler2>();
Is there a better solution? I am not a great fan of my idea, it is not very flexible.
services.AddHttpClient<Service1>()
.AddHttpMessageHandler(sp =>
{
var handler = sp.GetRequiredService<MyHandler>();
handler.Foo = "Bar";
return handler;
});
services.AddHttpClient<Service2>()
.AddHttpMessageHandler(sp =>
{
var handler = sp.GetRequiredService<MyHandler>();
handler.Foo = "Baz";
return handler;
});
I have the below Generic Class & Interface implementations
public interface IHttpClient<T> where T : class
{
public Task<List<T>> GetJsonAsync(string url);
}
public class HttpClient<T> : IHttpClient<T> where T:class
{
private readonly IHttpClientFactory _clientFactory;
public HttpClient(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<List<T>> GetJsonAsync(string url)
{
var request = new HttpRequestMessage(HttpMethod.Get,url);
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<T>>(result);
}
return null;
}
}
and this is how I try to register them in the startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped(typeof(IHttpClient<>), typeof(HttpClient<>));
}
My Controller:
private readonly IHttpClient<Feed> _feedClient;
public HomeController( IHttpClient<Feed> _client)
{
_feedClient = _client;
}
and this is the error I'm getting
InvalidOperationException: Unable to resolve service for type 'System.Net.Http.IHttpClientFactory' while attempting to activate...
what is it that I'm missing? any help is very appreciated..
You should register HttpClient in startup class like this
//register
services.AddHttpClient();
use
public YourController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
var client = _httpClientFactory.CreateClient();
Another options
//register
services.AddHttpClient("YourClientName", c =>
{
c.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
c.BaseAddress = new Uri("https://yoururl");
});
use
public YourController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
var client = _httpClientFactory.CreateClient("YourClientName");
I have a SessionService that has HttpClient injected into it and is registered as a Typed Client
See Microsoft example here.
I want to be able to write an integration test that I can control the responses made when the HttpClient is used.
I think that passing in a HttpMessageHandler to the HttpClient will allow me to intercept the request and control the response.
The problem I have is that I can't seem to add the HttpMessageHandler to the existing HttpClientFactory registration
// My client
public class SessionService
{
private readonly HttpClient httpClient;
public SessionService(HttpClient httpClient)
{
this.httpClient = httpClient;
}
public async Task<Session> GetAsync(string id)
{
var httpResponseMessage = await this.httpClient.GetAsync($"session/{id}");
var responseJson = await httpResponseMessage.Content?.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Session>(responseJson);
}
}
// Live registrations
public static class HttpModule
{
public static IServiceCollection AddHttpModule(this IServiceCollection serviceCollection) =>
serviceCollection
.AddHttpClient<SessionService>()
.Services;
}
// My HttpMessageHandler which controls the response
public class FakeHttpMessageHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
// customise response
return response;
}
}
If I try re-register the Typed Client so I can add the HttpMessageHandler it tells me I've already registered the client and can't register it again.
public static class TestHttpModule
{
public static IServiceCollection AddTestHttpModule(this IServiceCollection serviceCollection) =>
serviceCollection
.AddHttpClient<SessionService>() // <== Errors
.AddHttpMessageHandler<FakeHttpMessageHandler>()
.Services;
}
Any ideas?
The problem is because when you register a HttpClient using Dependency Injection, it adds it to an internal HttpClientMappingRegistry class. The fix was to remove the registration for the registry class. This allows me to re-add the Typed client and specify a HttpMessageHandler
public static class TestHttpModule
{
public static IServiceCollection AddTestHttpModule(this IServiceCollection serviceCollection) =>
serviceCollection
.AddSingleton(typeof(FakeHttpMessageHandler))
.RemoveHttpClientRegistry()
.AddHttpClient<SessionService>()
.AddHttpMessageHandler<FakeHttpMessageHandler>()
.Services;
private static IServiceCollection RemoveHttpClientRegistry(this IServiceCollection serviceCollection)
{
var registryType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())
.SingleOrDefault(t => t.Name == "HttpClientMappingRegistry");
var descriptor = serviceCollection.SingleOrDefault(x => x.ServiceType == registryType);
if (descriptor != null)
{
serviceCollection.Remove(descriptor);
}
return serviceCollection;
}
}
I created a fresh ASP.NET Core Web API project. Here is ConfigureServices in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddMemoryCache();
var serviceProvider = services.BuildServiceProvider();
var cache = serviceProvider.GetService<IMemoryCache>();
cache.Set("key1", "value1");
//_cahce.Count is 1
}
As you see I add an item to IMemoryCache. Here is my controller:
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IMemoryCache _cache;
public ValuesController(IMemoryCache cache)
{
_cache = cache;
}
[HttpGet("{key}")]
public ActionResult<string> Get(string key)
{
//_cahce.Count is 0
if(!_cache.TryGetValue(key, out var value))
{
return NotFound($"The value with the {key} is not found");
}
return value + "";
}
}
When I request https://localhost:5001/api/values/key1, the cache is empty and I receive a not found response.
In short, the cache instance you're setting the value in is not the same as the one that is later being retrieved. You cannot do stuff like while the web host is being built (i.e. in ConfigureServices/Configure. If you need to do something on startup, you need to do it after the web host is built, in Program.cs:
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
var cache = host.Services.GetRequiredService<IMemoryCache>();
cache.Set("key1", "value1");
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
}
As #selloape saids, if you manullay call BuildServicesProvider, you are creating a new provider, that will be not used in your controllers.
You can use a hosted service to intialize your cache
public class InitializeCacheService : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public InitializeCacheService (IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task StartAsync(CancellationToken cancellationToken)
{
using (var scope = _serviceProvider.CreateScope())
{
var cache = _serviceProvider.GetService<IMemoryCache>();
cache.Set("key1", "value1");
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
Add it in your ConfigureServices
services.AddHostedService<InitializeCacheService>();
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.2
I have problems performing unit testing when using Polly and HttpClient.
Specifically, Polly and HttpClient are used for ASP.NET Core Web API Controller following the links below:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests
https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory
The problems (1 and 2) are specified at the bottom. I wonder if this is the correct way to using Polly and HttpClient.
ConfigureServices
Configure Polly policy and sepecify custom HttpClient, CustomHttpClient
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
services.AddHttpClient<HttpClientService>()
.AddPolicyHandler((service, request) =>
HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(3,
retryCount => TimeSpan.FromSeconds(Math.Pow(2, retryCount)))
);
}
CarController
CarController depends on HttpClientService, which is injected by the framework automatically without explicit registration.
Please note that HttpClientService causes issues with unit test as Moq cannot mock a non virtual method, which is mentioned later on.
[ApiVersion("1")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class CarController : ControllerBase
{
private readonly ILog _logger;
private readonly HttpClientService _httpClientService;
private readonly IOptions<Config> _config;
public CarController(ILog logger, HttpClientService httpClientService, IOptions<Config> config)
{
_logger = logger;
_httpClientService = httpClientService;
_config = config;
}
[HttpPost]
public async Task<ActionResult> Post()
{
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
{
string body = reader.ReadToEnd();
var statusCode = await _httpClientService.PostAsync(
"url",
new Dictionary<string, string>
{
{"headerID", "Id"}
},
body);
return StatusCode((int)statusCode);
}
}
}
HttpClientService
Simiar to CarController, HttpClientService has an issue with Unit test, because HttpClient's PostAsync cannot be mocked. HttpClient is injected by the framework automatically without explicit registration.
public class HttpClientService
{
private readonly HttpClient _client;
public HttpClientService(HttpClient client)
{
_client = client;
}
public async Task<HttpStatusCode> PostAsync(string url, Dictionary<string, string> headers, string body)
{
using (var content = new StringContent(body, Encoding.UTF8, "application/json"))
{
foreach (var keyValue in headers)
{
content.Headers.Add(keyValue.Key, keyValue.Value);
}
var response = await _client.PostAsync(url, content);
response.EnsureSuccessStatusCode();
return response.StatusCode;
}
}
Problem 1
Unit Test: Moq cannot mock HttpClientService's PostAsync method. I CAN change it to virtual, but I wonder if it is the best option.
public class CarControllerTests
{
private readonly Mock<ILog> _logMock;
private readonly Mock<HttpClient> _httpClientMock;
private readonly Mock<HttpClientService> _httpClientServiceMock;
private readonly Mock<IOptions<Config>> _optionMock;
private readonly CarController _sut;
public CarControllerTests() //runs for each test method
{
_logMock = new Mock<ILog>();
_httpClientMock = new Mock<HttpClient>();
_httpClientServiceMock = new Mock<HttpClientService>(_httpClientMock.Object);
_optionMock = new Mock<IOptions<Config>>();
_sut = new CarController(_logMock.Object, _httpClientServiceMock.Object, _optionMock.Object);
}
[Fact]
public void Post_Returns200()
{
//System.NotSupportedException : Invalid setup on a non-virtual (overridable in VB) member
_httpClientServiceMock.Setup(hc => hc.PostAsync(It.IsAny<string>(),
It.IsAny<Dictionary<string, string>>(),
It.IsAny<string>()))
.Returns(Task.FromResult(HttpStatusCode.OK));
}
}
}
Problem 2
Unit test: similar to HttpClientService, Moq cannot mock HttpClient's PostAsync method.
public class HttpClientServiceTests
{
[Fact]
public void Post_Returns200()
{
var httpClientMock = new Mock<HttpClient>();
//System.NotSupportedException : Invalid setup on a non-virtual (overridable in VB) member
httpClientMock.Setup(hc => hc.PostAsync("", It.IsAny<HttpContent>()))
.Returns(Task.FromResult(new HttpResponseMessage()));
}
}
ASP.NET Core API 2.2
Update
Corrected a typo of CustomHttpClient to HttpClientService
Update 2
Solution to problem 2
Assuming CustomHttpClient is a typo an the HttpClientService is the actual dependency, the controller is tightly coupled to implementation concerns which, as you have already experiences, are difficult to test in isolation (unit test).
Encapsulate those concretions behind abstractions
public interface IHttpClientService {
Task<HttpStatusCode> PostAsync(string url, Dictionary<string, string> headers, string body);
}
public class HttpClientService : IHttpClientService {
//...omitted for brevity
}
that can be replaced when testing.
Refactor the controller to depend on the abstraction and not the concrete implementation
public class CarController : ControllerBase {
private readonly ILog _logger;
private readonly IHttpClientService httpClientService; //<-- TAKE NOTE
private readonly IOptions<Config> _config;
public CarController(ILog logger, IHttpClientService httpClientService, IOptions<Config> config) {
_logger = logger;
this.httpClientService = httpClientService;
_config = config;
}
//...omitted for brevity
}
Update the service configuration to use the overload that allows the registration of the abstraction along with its implementation
public void ConfigureServices(IServiceCollection services) {
services.AddHttpClient();
services
.AddHttpClient<IHttpClientService, HttpClientService>() //<-- TAKE NOTE
.AddPolicyHandler((service, request) =>
HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(3,
retryCount => TimeSpan.FromSeconds(Math.Pow(2, retryCount)))
);
}
That takes care of refatcoring the code to make it more test friendly.
The following shows how the controller can now be unit tested in isolation
public class CarControllerTests {
private readonly Mock<ILog> _logMock;
private readonly Mock<IHttpClientService> _httpClientServiceMock;
private readonly Mock<IOptions<Config>> _optionMock;
private readonly CarController _sut;
public CarControllerTests() //runs for each test method
{
_logMock = new Mock<ILog>();
_httpClientServiceMock = new Mock<IHttpClientService>();
_optionMock = new Mock<IOptions<Config>>();
_sut = new CarController(_logMock.Object, _httpClientServiceMock.Object, _optionMock.Object);
}
[Fact]
public async Task Post_Returns200() {
//Arrange
_httpClientServiceMock
.Setup(_ => _.PostAsync(
It.IsAny<string>(),
It.IsAny<Dictionary<string, string>>(),
It.IsAny<string>())
)
.ReturnsAsync(HttpStatusCode.OK);
//Act
//...omitted for brevity
//...
}
}
Note how there was no longer a need for the HttpClient for the test to be exercised to completion.
There are other dependencies that the controller will need to make sure are arranged for the test to flow but that is currently ouside of the scope of the question.