Como parte de un proyecto de ASP.Net Core en el que estoy trabajando, tengo el requisito de comunicarme con varios puntos finales de API basados en Rest diferentes desde mi WebApi. Para lograr esto, estoy usando una serie de clases de servicio que instancian un HttpClient estático. Esencialmente, tengo una clase de servicio para cada uno de los puntos finales basados en Rest a los que se conecta WebApi.
A continuación se puede ver un ejemplo de cómo se crea una instancia del HttpClient estático en cada una de las clases de servicio.
private static HttpClient _client = new HttpClient() { BaseAddress = new Uri("http://endpointurlexample"), }; Si bien lo anterior funciona bien, no permite realizar pruebas unitarias efectivas de las clases de servicio que utilizan HttpClient . Para permitirme llevar a cabo pruebas unitarias, tengo un HttpMessageHandler falso que me gustaría usar para HttpClient en mis pruebas unitarias, mientras que HttpClient se instancia como se indicó anteriormente; sin embargo, no puedo aplicar el HttpMessageHandler falso como parte de mis pruebas unitarias.
¿Cuál es la mejor manera para que HttpClient en las clases de servicio siga siendo una sola instancia en toda la aplicación (una instancia por punto final), pero permita que se aplique un HttpMessageHandler diferente durante las pruebas unitarias?
Un enfoque en el que he pensado sería no usar un campo estático para mantener el HttpClient en las clases de servicio, sino permitir que se inyecte a través de la inyección del constructor usando un ciclo de vida único, lo que me permitiría especificar un HttpClient con el HttpMessageHandler deseado durante las pruebas unitarias, la otra opción en la que pensé sería usar una clase de fábrica HttpClient que creara una instancia de HttpClient s en campos estáticos que luego podrían recuperarse inyectando la fábrica HttpClient en las clases de servicio, lo que nuevamente permite una implementación diferente con el HttpMessageHandler relevante para ser devuelto en pruebas unitarias. Sin embargo, ninguno de los anteriores se siente particularmente limpio y parece que debe haber una mejor manera.
Cualquier pregunta, hágamelo saber.
Agregar a la conversación desde los comentarios parece que necesitaría una fábrica HttpClient
public interface IHttpClientFactory { HttpClient Create(string endpoint); }y la implementación de la funcionalidad central podría verse así.
public class DefaultHttpClientFactory : IHttpClientFactory, IDisposable { private readonly ConcurrentDictionary<string, HttpClient> _httpClients; public DefaultHttpClientFactory() { this._httpClients = new ConcurrentDictionary<string, HttpClient>(); } public HttpClient Create(string endpoint) { if (this._httpClients.TryGetValue(endpoint, out var client)) { return client; } client = new HttpClient { BaseAddress = new Uri(endpoint), }; this._httpClients.TryAdd(endpoint, client); return client; } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { foreach (var httpClient in this._httpClients) { httpClient.Value.Dispose(); } } } Dicho esto, si no está particularmente satisfecho con el diseño anterior. Puede abstraer la dependencia de HttpClient detrás de un servicio para que el cliente no se convierta en un detalle de implementación.
Que los consumidores del servicio no necesitan saber exactamente cómo se recuperan los datos.
Piensas complicado. Todo lo que necesita es una fábrica HttpClient o un accesorio con una propiedad HttpClient y usarlo de la misma manera que ASP.NET Core permite que se inyecte HttpContext
public interface IHttpClientAccessor { HttpClient Client { get; } } public class DefaultHttpClientAccessor : IHttpClientAccessor { public HttpClient Client { get; } public DefaultHttpClientAccessor() { Client = new HttpClient(); } }e inyectar esto en sus servicios
public class MyRestClient : IRestClient { private readonly HttpClient client; public MyRestClient(IHttpClientAccessor httpClientAccessor) { client = httpClientAccessor.Client; } }registro en Startup.cs:
services.AddSingleton<IHttpClientAccessor, DefaultHttpClientAccessor>();Para pruebas unitarias, simplemente simule
// Moq-esque // Arrange var httpClientAccessor = new Mock<IHttpClientAccessor>(); var httpHandler = new HttpMessageHandler(..) { ... }; var httpContext = new HttpContext(httpHandler); httpClientAccessor.SetupGet(a => a.Client).Returns(httpContext); // Act var restClient = new MyRestClient(httpClientAccessor.Object); var result = await restClient.GetSomethingAsync(...); // Assert ...Mi preferencia actual es derivar de HttpClient una vez por dominio de punto final de destino y convertirlo en un único mediante la inyección de dependencia en lugar de usar HttpClient directamente.
Digamos que estoy haciendo solicitudes HTTP a example.com, tendría un ExampleHttpClient que hereda de HttpClient y tiene la misma firma de constructor que HttpClient , lo que le permite pasar y burlarse de HttpMessageHandler como de costumbre.
public class ExampleHttpClient : HttpClient { public ExampleHttpClient(HttpMessageHandler handler) : base(handler) { BaseAddress = new Uri("http://example.com"); // set default headers here: content type, authentication, etc } } Luego configuro ExampleHttpClient como singleton en mi registro de inyección de dependencia y agrego un registro para HttpMessageHandler como transitorio, ya que se creará solo una vez por tipo de cliente http. Usando este patrón, no necesito tener múltiples registros complicados para HttpClient o fábricas inteligentes para construirlos según el nombre del host de destino.
Cualquier cosa que necesite hablar con example.com debe tener una dependencia de constructor en ExampleHttpClient y luego todos comparten la misma instancia y obtienes la agrupación de conexiones según lo diseñado.
De esta manera, también le brinda un lugar más agradable para colocar cosas como encabezados predeterminados, tipos de contenido, autorización, dirección base, etc., y ayuda a evitar que la configuración http de un servicio se filtre a otro servicio.