7

ASP.Net CoreでHttpClientクラスを最適に使用する方法を理解しようとしています。ASP.NetコアでHTTPClientをDIシングルトンとして使用する最良の方法

ドキュメントおよびいくつかの記事によれば、クラスはアプリケーションの存続期間中一度だけインスタンス化され、複数の要求に対して共有されます。残念ながら、私はコアでこれを正しく行う方法の例を見つけることができませんでしたので、私は以下の解決策を考案しました。

私は、2つの異なるエンドポイント(ビジネスロジックとAPI駆動型ImageServer用のAPIServerを使用しています)を使用する必要があるので、私はアプリケーションで使用できる2つのHttpClientシングルトンを持つことを考えています。次のように私はappsettings.jsonで私servicepointsを構成した

"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を使用してイメージサーバからイメージを要求する例を示します。

[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: 'このタイプのpublic static(Visual Basic Basicで共有されている)メンバーはスレッドセーフです。')
  2. この設定では、多くの個別の接続を開かずに負荷を処理できますか?
  3. 'Singleton HttpClient?'で説明されているDNSの問題を処理するASP.Netコアで何をすればよいですか?この重大な行動や修正方法に注意してください。」http://byterot.blogspot.be/2016/07/singleton-httpclient-dns.html
  4. 他の改善や提案はありますか?
+0

興味深いアプローチをアップロードするユーザーを扱う

[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は、私はサービスとしてHTTPClientの静的メソッドを持っているが、私はへこみファクトリパターンを考えます。私は認証を必要とするAPIの下でこれを試してみるべきか、あるいはあなたのAPIのケースがOpen APIであるのだろうと思っていましたか?異なるトークンを必要とするさまざまな要求に対して、これをどのように処理しますか? –

+0

@MuqeetKhanあなたの質問に対する私の答えは、もうちょっと待ってから予想していました。だから、下の例を見てください。 – Laobu

+0

2つの 'HttpClient'インスタンスは必要ありません。ただ1つのシングルトンを登録して使用してください。 DNSの問題は引き続き存在します。 sidenoteとして、 'Factory'クラス**はオブジェクトインスタンスを作成します**。 Factoryパターン[here](https://msdn.microsoft.com/en-us/library/ee817667.aspx)の詳細を参照してください。 – gldraphael

答えて

0

httpClientリクエストで認証を使用することに関する@MuqeetKhanからの質問に対する回答です。

DIと工場を使用する私のモチベーションは、アプリケーションをさまざまなAPIに簡単に拡張し、コード全体に簡単にアクセスできるようにすることでした。私は複数回再利用できることを願っています。

私の 'GetUserPicture'コントローラの場合、上記の最初の質問に記載されていますが、私は確かに簡単な理由から認証を削除しました。正直なところ、イメージサーバーから単にイメージを取得する必要があるのであれば、私はまだ疑問に思っています。とにかく、他のコントローラでは、私は間違いなくそれを必要とするので、...

認証サーバーとしてIdentityserver4を実装しました。これにより、ASP IDの上に認証が提供されます。 権限(この場合はロールを使用)については、私は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リクエストパイプラインで1回、APIリクエストパイプラインで1回。

この設定を使用して、認証と認証でHttpClientsFactoryを使用することができます。

大きなセキュリティ上の部分で私はもちろんHTTPSがありません。私は何とか私の工場にそれを追加することを願っています。私はそれを実装した後、私はそれを更新します。

いつもどんな提案も歓迎します。

以下の例では、認証を使用してImagesServerに画像をアップロードします(ログインし、ロール管理者である必要があります)。 「UploadUserPicture」呼び出し

私のMVCコントローラ:

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