C# 使用特定的HttpMessageHandler注入单实例HttpClient
作为我正在从事的ASP.Net核心项目的一部分,我需要从我的WebApi中与许多不同的基于Rest的API端点进行通信。为了实现这一点,我使用了许多服务类,每个服务类实例化一个静态的C# 使用特定的HttpMessageHandler注入单实例HttpClient,c#,asp.net-core,asp.net-core-mvc,.net-core,dotnet-httpclient,C#,Asp.net Core,Asp.net Core Mvc,.net Core,Dotnet Httpclient,作为我正在从事的ASP.Net核心项目的一部分,我需要从我的WebApi中与许多不同的基于Rest的API端点进行通信。为了实现这一点,我使用了许多服务类,每个服务类实例化一个静态的HttpClient。实际上,我为WebApi连接到的每个基于Rest的端点都有一个服务类 下面可以看到如何在每个服务类中实例化staticHttpClient private static HttpClient _client = new HttpClient() { BaseAddress = new U
HttpClient
。实际上,我为WebApi连接到的每个基于Rest的端点都有一个服务类
下面可以看到如何在每个服务类中实例化staticHttpClient
private static HttpClient _client = new HttpClient()
{
BaseAddress = new Uri("http://endpointurlexample"),
};
尽管上述方法运行良好,但它不允许对使用HttpClient
的服务类进行有效的单元测试。为了使我能够执行单元测试,我有一个假的HttpMessageHandler
,我想在我的单元测试中用于HttpClient
,而HttpClient
如上实例化,但是我无法将假的HttpMessageHandler
作为单元测试的一部分
服务类中的HttpClient
在整个应用程序中保持单个实例(每个端点一个实例)但允许在单元测试期间应用不同的HttpMessageHandler
的最佳方式是什么
我想到的一种方法是不使用静态字段在服务类中保存HttpClient
,而是允许使用单例生命周期通过构造函数注入来注入它,这将允许我在单元测试期间使用所需的HttpMessageHandler
指定HttpClient
,我想到的另一个选项是使用HttpClient
工厂类,该类在静态字段中实例化了HttpClient
s,然后可以通过将HttpClient
工厂注入服务类中来检索这些字段,再次允许在单元测试中返回具有相关HttpMessageHandler
的不同实现。但是,上面的这些感觉都不是特别干净,而且感觉一定有更好的方法吗
如果有任何问题,请告诉我。你觉得很复杂。您所需要的只是一个带有
HttpClient
属性的HttpClient工厂或访问器,并以ASP.NET内核允许注入HttpContext
的方式使用它
public interface IHttpClientAccessor
{
HttpClient Client { get; }
}
public class DefaultHttpClientAccessor : IHttpClientAccessor
{
public HttpClient Client { get; }
public DefaultHttpClientAccessor()
{
Client = new HttpClient();
}
}
并将其注入到您的服务中
public class MyRestClient : IRestClient
{
private readonly HttpClient client;
public MyRestClient(IHttpClientAccessor httpClientAccessor)
{
client = httpClientAccessor.Client;
}
}
在Startup.cs中注册:
services.AddSingleton<IHttpClientAccessor, DefaultHttpClientAccessor>();
services.AddSingleton();
对于单元测试,只需模拟它
// 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
...
//Moq-esque
//安排
var httpClientAccessor=new Mock();
var httpHandler=newhttpmessagehandler(..){…};
var httpContext=新的httpContext(httpHandler);
SetupGet(a=>a.Client).Returns(httpContext);
//表演
var restClient=newmyrestclient(httpClientAccessor.Object);
var result=await restClient.GetSomethingAsync(…);
//断言
...
从评论中添加到对话中看起来需要一个HttpClient
工厂
public interface IHttpClientFactory {
HttpClient Create(string endpoint);
}
核心功能的实现可以是这样的
public类DefaultHttpClientFactory:IHttpClientFactory,IDisposable
{
私有只读ConcurrentDictionary\u HttpClient;
公共默认HttpClientFactory()
{
这是._httpClients=新的ConcurrentDictionary();
}
公共HttpClient创建(字符串端点)
{
if(this.\u httpClients.TryGetValue(端点,out var client))
{
返回客户;
}
client=新的HttpClient
{
BaseAddress=新Uri(端点),
};
这是.u httpClients.TryAdd(端点,客户端);
返回客户;
}
公共空间处置()
{
这个。处置(真实);
总干事(本);
}
受保护的虚拟void Dispose(bool disposing)
{
foreach(此中的var httpClient.\u httpClients)
{
httpClient.Value.Dispose();
}
}
}
也就是说,如果您对上述设计不是特别满意。您可以抽象掉服务背后的HttpClient
依赖关系,这样客户端就不会成为实现细节
服务的消费者不需要确切地知道数据是如何检索的。我可能会迟到,但我已经创建了一个Helper nuget包,允许您在单元测试中测试HttpClient端点 NuGet:
安装程序包worlddomation.HttpClient.Helpers
回购: 基本思想是创建伪响应负载,并将a
FakeHttpMessageHandler
实例传递给代码,其中包括该伪响应负载。然后,当您的代码试图实际命中URI端点时,它不会。。。而只是返回假响应。魔法
这里有一个非常简单的例子:
[Fact]
public async Task GivenSomeValidHttpRequests_GetSomeDataAsync_ReturnsAFoo()
{
// Arrange.
// Fake response.
const string responseData = "{ \"Id\":69, \"Name\":\"Jane\" }";
var messageResponse = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData);
// Prepare our 'options' with all of the above fake stuff.
var options = new HttpMessageOptions
{
RequestUri = MyService.GetFooEndPoint,
HttpResponseMessage = messageResponse
};
// 3. Use the fake response if that url is attempted.
var messageHandler = new FakeHttpMessageHandler(options);
var myService = new MyService(messageHandler);
// Act.
// NOTE: network traffic will not leave your computer because you've faked the response, above.
var result = await myService.GetSomeFooDataAsync();
// Assert.
result.Id.ShouldBe(69); // Returned from GetSomeFooDataAsync.
result.Baa.ShouldBeNull();
options.NumberOfTimesCalled.ShouldBe(1);
}
我目前的偏好是从
HttpClient
中派生出每个目标端点域一次,并使用依赖项注入使其成为一个单例,而不是直接使用HttpClient
假设我向example.com发出HTTP请求,我会有一个ExampleHttpClient
,它继承自HttpClient
,并具有与HttpClient
相同的构造函数签名,允许您正常传递和模拟HttpMessageHandler
public class ExampleHttpClient : HttpClient
{
public ExampleHttpClient(HttpMessageHandler handler) : base(handler)
{
BaseAddress = new Uri("http://example.com");
// set default headers here: content type, authentication, etc
}
}
然后,我在依赖项注入注册中将ExampleHttpClient
设置为singleton,并为HttpMessageHandler
添加一个注册,该注册是瞬态的,因为每个http客户端类型只创建一次。使用此模式,我不需要为HttpClient
或智能工厂进行多个复杂的注册,就可以基于目标主机名构建它们
任何需要与example.com对话的内容都应该在ExampleHttpClient
上建立构造函数依赖关系,然后它们都共享同一个实例,您就可以按照设计得到连接池
这边
public class CustomAuthorizationAttribute : Attribute, IAuthorizationFilter
{
private string Roles;
private static readonly HttpClient _httpClient = new HttpClient();