在ASP.Net Core中使用HTTPClient作为DI Singleton

时间:2017-02-06 10:37:30

标签: c# asp.net-core dotnet-httpclient

我试图弄清楚如何在ASP.Net Core中最好地使用HttpClient类。

根据文档和几篇文章,该类最好在应用程序的生命周期中实例化一次,并为多个请求共享。不幸的是,我找不到如何在Core中正确执行此操作的示例,因此我提出了以下解决方案。

我的特殊需求需要使用2个不同的端点(我有一个用于业务逻辑的APIServer和一个API驱动的ImageServer),所以我的想法是拥有2个可以在应用程序中使用的HttpClient单例。

我已经在appsettings.json中配置了我的服务点,如下所示:

"ServicePoints": {
"APIServer": "http://localhost:5001",
"ImageServer": "http://localhost:5002",
}

接下来,我创建了一个HttpClientsFactory,它将实例化我的2个httpclients并将它们保存在静态字典中。

public class HttpClientsFactory : IHttpClientsFactory
{
    public static Dictionary<string, HttpClient> HttpClients { get; set; }
    private readonly ILogger _logger;
    private readonly IOptions<ServerOptions> _serverOptionsAccessor;

    public HttpClientsFactory(ILoggerFactory loggerFactory, IOptions<ServerOptions> serverOptionsAccessor) {
        _logger = loggerFactory.CreateLogger<HttpClientsFactory>();
        _serverOptionsAccessor = serverOptionsAccessor;
        HttpClients = new Dictionary<string, HttpClient>();
        Initialize();
    }

    private void Initialize()
    {
        HttpClient client = new HttpClient();
        // ADD imageServer
        var imageServer = _serverOptionsAccessor.Value.ImageServer;
        client.BaseAddress = new Uri(imageServer);
        HttpClients.Add("imageServer", client);

        // ADD apiServer
        var apiServer = _serverOptionsAccessor.Value.APIServer;
        client.BaseAddress = new Uri(apiServer);
        HttpClients.Add("apiServer", client);
    }

    public Dictionary<string, HttpClient> Clients()
    {
        return HttpClients;
    }

    public HttpClient Client(string key)
    {
        return Clients()[key];
    }
  } 

然后,我创建了以后在定义我的DI时可以使用的界面。请注意,HttpClientsFactory类继承自此接口。

public interface IHttpClientsFactory
{
    Dictionary<string, HttpClient> Clients();
    HttpClient Client(string key);
}

现在我准备将它注入我的Dependency容器,如下所示,在ConfigureServices方法下的Startup类中。

// Add httpClient service
        services.AddSingleton<IHttpClientsFactory, HttpClientsFactory>();

所有现在都设置为在我的控制器中开始使用它 首先,我接受了依赖。为此,我创建了一个私有类属性来保存它,然后将它添加到构造函数签名中,并通过将传入对象分配给本地类属性来完成。

private IHttpClientsFactory _httpClientsFactory;
public AppUsersAdminController(IHttpClientsFactory httpClientsFactory)
{
   _httpClientsFactory = httpClientsFactory;
}

最后,我们现在可以使用Factory来请求htppclient并执行调用。下面是我使用httpclientsfactory:

从imageserver请求图像的示例
[HttpGet]
    public async Task<ActionResult> GetUserPicture(string imgName)
    {
        // get imageserver uri
        var imageServer = _optionsAccessor.Value.ImageServer;

        // create path to requested image
        var path = imageServer + "/imageuploads/" + imgName;

        var client = _httpClientsFactory.Client("imageServer");
        byte[] image = await client.GetByteArrayAsync(path);

        return base.File(image, "image/jpeg");
    }

完成!

我已经对此进行了测试,它在我的开发环境中运行良好。但是,我不确定这是否是实现此目的的最佳方式。我仍然提出以下问题:

  1. 此解决方案线程是否安全? (根据MS doc:'任何公共静态(在Visual Basic中共享)此类型的成员都是线程安全的。')
  2. 这种设置是否能够在不打开多个单独连接的情况下处理重负载?
  3. 在ASP.Net核心中如何处理'Singleton HttpClient中描述的DNS问题?请注意这种严重的行为以及如何解决。“位于http://byterot.blogspot.be/2016/07/singleton-httpclient-dns.html
  4. 任何其他改进或建议?

3 个答案:

答案 0 :(得分:1)

回答@MuqeetKhan关于在httpclient请求中使用身份验证的问题。

首先,我使用DI和工厂的动机是允许我轻松地将我的应用程序扩展到不同的API和多个API,并且可以在我的代码中轻松访问它。这是一个模板,我希望能够重复使用多次。

在我上面的原始问题中描述的'GetUserPicture'控制器的情况下,我确实为了简单起见删除了身份验证。但老实说,如果我需要它来简单地从图像服务器中检索图像,我仍然有疑问。无论如何,在其他控制器中我肯定需要它,所以......

我已将Identityserver4作为我的身份验证服务器实现。这为我提供了ASP身份验证。 对于授权(在这种情况下使用角色),我在我的MVC'和'API项目中实现了IClaimsTransformer(您可以在How to put ASP.net Identity Roles into the Identityserver4 Identity token处阅读更多相关信息)。

现在,当我进入我的控制器时,我有一个经过身份验证和授权的用户,我可以为其检索访问令牌。我使用此令牌来调用我的api,这当然是调用identityserver的同一个实例来验证用户是否经过身份验证。

最后一步是允许我的API验证用户是否有权调用所请求的api控制器。在使用IClaimsTransformer的API的请求管道中,如前所述,我检索主叫用户的授权并将其添加到传入的声明中。 请注意,在MVC调用和API的情况下,我因此检索授权2次;一次在MVC请求管道中,一次在API请求管道中。

使用此设置,我可以将HttpClientsFactory与授权和身份验证一起使用。

在我失踪的大安全部分当然是HTTPS。我希望我能以某种方式将它添加到我的工厂。我已经实施了它,我会更新它。

一如既往,欢迎任何建议。

下面我使用身份验证将图像上传到Imageserver的示例(用户必须登录并拥有角色admin)。

我的MVC控制器调用'UploadUserPicture':

    [Authorize(Roles = "Admin")]
    [HttpPost]
    public async Task<ActionResult> UploadUserPicture()
    {
        // collect name image server
        var imageServer = _optionsAccessor.Value.ImageServer;

        // collect image in Request Form from Slim Image Cropper plugin
        var json = _httpContextAccessor.HttpContext.Request.Form["slim[]"];

        // Collect access token to be able to call API
        var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");

        // prepare api call to update image on imageserver and update database
        var client = _httpClientsFactory.Client("imageServer");
        client.DefaultRequestHeaders.Accept.Clear();
        client.SetBearerToken(accessToken);
        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("image", json[0])
        });
        HttpResponseMessage response = await client.PostAsync("api/UserPicture/UploadUserPicture", content);

        if (response.StatusCode != HttpStatusCode.OK)
        {
            return StatusCode((int)HttpStatusCode.InternalServerError);
        }
        return StatusCode((int)HttpStatusCode.OK);
    }

处理用户上传的API

    [Authorize(Roles = "Admin")]
    [HttpPost]
    public ActionResult UploadUserPicture(String image)
    {
     dynamic jsonDe = JsonConvert.DeserializeObject(image);

        if (jsonDe == null)
        {
            return new StatusCodeResult((int)HttpStatusCode.NotModified);
        }

        // create filname for user picture
        string userId = jsonDe.meta.userid;
        string userHash = Hashing.GetHashString(userId);
        string fileName = "User" + userHash + ".jpg";

        // create a new version number
        string pictureVersion = DateTime.Now.ToString("yyyyMMddHHmmss");

        // get the image bytes and create a memory stream
        var imagebase64 = jsonDe.output.image;
        var cleanBase64 = Regex.Replace(imagebase64.ToString(), @"^data:image/\w+;base64,", "");
        var bytes = Convert.FromBase64String(cleanBase64);
        var memoryStream = new MemoryStream(bytes);

        // save the image to the folder
        var fileSavePath = Path.Combine(_env.WebRootPath + ("/imageuploads"), fileName);
        FileStream file = new FileStream(fileSavePath, FileMode.Create, FileAccess.Write);
        try
        {
            memoryStream.WriteTo(file);
        }
        catch (Exception ex)
        {
            _logger.LogDebug(LoggingEvents.UPDATE_ITEM, ex, "Could not write file >{fileSavePath}< to server", fileSavePath);
            return new StatusCodeResult((int)HttpStatusCode.NotModified);
        }
        memoryStream.Dispose();
        file.Dispose();
        memoryStream = null;
        file = null;

        // update database with latest filename and version
        bool isUpdatedInDatabase = UpdateDatabaseUserPicture(userId, fileName, pictureVersion).Result;

        if (!isUpdatedInDatabase)
        {
            return new StatusCodeResult((int)HttpStatusCode.NotModified);
        }

        return new StatusCodeResult((int)HttpStatusCode.OK);
    }

答案 1 :(得分:1)

如果使用.net core 2.1或更高版本,最好的方法是使用新的HttpClientFactory。我想微软意识到人们遇到的所有问题,所以他们为我们做了艰苦的工作。有关如何设置的信息,请参见下文。

注意:添加对Microsoft.Extensions.Http的引用。

1-添加使用HttpClient的类

public interface ISomeApiClient
{
    Task<HttpResponseMessage> GetSomethingAsync(string query);
}

public class SomeApiClient : ISomeApiClient
{
    private readonly HttpClient _client;

    public SomeApiClient (HttpClient client)
    {
        _client = client;
    }

    public async Task<SomeModel> GetSomethingAsync(string query)
    {
        var response = await _client.GetAsync($"?querystring={query}");
        if (response.IsSuccessStatusCode)
        {
            var model = await response.Content.ReadAsJsonAsync<SomeModel>();
            return model;
        }
        // Handle Error
    }
}

2-在{upup.cs}的ConfigureServices(IServiceCollection services)中注册客户

var someApiSettings = Configuration.GetSection("SomeApiSettings").Get<SomeApiSettings>(); //Settings stored in app.config (base url, api key to add to header for all requests)
services.AddHttpClient<ISomeApiClient, SomeApiClient>("SomeApi",
                client =>
                {
                    client.BaseAddress = new Uri(someApiSettings.BaseAddress);
                    client.DefaultRequestHeaders.Add("api-key", someApiSettings.ApiKey);
                });

3-在代码中使用客户端

public class MyController
{
    private readonly ISomeApiClient _client;

    public MyController(ISomeApiClient client)
    {
        _client = client;
    }

    [HttpGet]
    public async Task<IActionResult> GetAsync(string query)
    {
        var response = await _client.GetSomethingAsync(query);

        // Do something with response

        return Ok();
    }
}

您可以使用services.AddHttpClient

在启动时添加任意数量的客户端并注册所需的数量。

感谢Steve Gordon和his post here帮助我在代码中使用它!

答案 2 :(得分:0)

对于无法使用 DI 的情况:
    using System.Net.Http;

    public class SomeClass
    {
        private static readonly HttpClient Client;

        static SomeClass()
        {
            var handler = new SocketsHttpHandler
            {
                // Sets how long a connection can be in the pool to be considered reusable (by default - infinite)
                PooledConnectionLifetime = TimeSpan.FromMinutes(1),
            };

            Client = new HttpClient(handler, disposeHandler: false);
        }
        
        ...
    }

参考https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0#alternatives-to-ihttpclientfactory