web-dev-qa-db-ja.com

ASP.Net CoreでHTTPClientをDIシングルトンとして使用する最良の方法

ASP.Net CoreでHttpClientクラスを最適に使用する方法を理解しようとしています。

ドキュメントといくつかの記事によると、クラスはアプリケーションの存続期間中に一度インスタンス化され、複数のリクエストで共有されるのが最適です。残念ながら、Coreでこれを正しく行う方法の例が見つからなかったため、次の解決策を考え出しました。

私の特定のニーズでは、2つの異なるエンドポイント(ビジネスロジック用のAPIServerとAPI駆動のImageServerがある)を使用する必要があるため、アプリケーションで使用できる2つのHttpClientシングルトンを使用することを考えています。

Appsettings.jsonでサービスポイントを次のように構成しました。

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

次に、2つのhttpclientsをインスタンス化して静的ディクショナリに保持するHttpClientsFactoryを作成しました。

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);
}

これで、ConfigureServicesメソッドの下のStartupクラスで、次のようにDependencyコンテナーにこれを注入する準備ができました。

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

これで、コントローラーでこれを使用するようにセットアップされました。
まず、依存関係を取り入れます。これを行うには、それを保持するプライベートクラスプロパティを作成し、それをコンストラクタシグネチャに追加して、受信オブジェクトをローカルクラスプロパティに割り当てて終了します。

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

最後に、ファクトリを使用して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のドキュメントによれば、「このタイプのすべてのpublic static(Visual BasicではShared)メンバーはスレッドセーフです。」)
  2. このセットアップは、多くの個別の接続を開かなくても重い負荷を処理できますか?
  3. ASP.Netコアで「シングルトンHttpClient」で説明されているDNSの問題を処理するにはどうすればよいですか?この深刻な動作と修正方法に注意してください。」 http://byterot.blogspot.be/2016/07/singleton-httpclient-dns.html にあります
  4. 他の改善や提案はありますか?
12
Laobu

.netコア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-Startup.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と 彼の投稿はこちら に感謝します。

2
garethb

@MuqeetKhanからのhttpclientリクエストでの認証の使用に関する質問への回答。

まず、DIとファクトリを使用する動機は、アプリケーションをさまざまな複数のAPIに簡単に拡張し、コード全体で簡単にアクセスできるようにすることでした。これは、何度も再利用できるようにしたいテンプレートです。

上記の元の質問で説明した「GetUserPicture」コントローラーの場合、私は確かに簡単にするために認証を削除しました。しかし正直なところ、単純にイメージサーバーから画像を取得するために必要なのかどうかはまだわかりません。とにかく、他のコントローラーでは間違いなく必要なので、…

Identityserver4を認証サーバーとして実装しました。これにより、ASP Identity。の上に認証が提供されます。承認(この場合はロールを使用)のために、IClaimsTransformerをMVCの「および」APIプロジェクトに実装しました(この詳細については、こちらを参照してください) at ASP.net IDロールをIdentityserver4 IDトークンに入れる方法 )。

これで、コントローラーに入るとすぐに、アクセストークンを取得できる認証済みユーザーと承認済みユーザーができました。このトークンを使用してapiを呼び出します。もちろん、これはidentityserverの同じインスタンスを呼び出して、ユーザーが認証されているかどうかを確認します。

最後のステップは、ユーザーが要求されたAPIコントローラーを呼び出すことをユーザーに許可されているかどうかをAPIが確認できるようにすることです。前に説明したように、IClaimsTransformerを使用するAPIのリクエストパイプラインで、呼び出し元ユーザーの認証を取得し、それを受信クレームに追加します。 MVC呼び出しとAPIの場合は、このように2回認証を取得することに注意してください。 1つはMVCリクエストパイプラインで、もう1つは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
Laobu