7

ASP.Net 코어에서 HttpClient 클래스를 가장 효과적으로 사용하는 방법을 찾으려고합니다.ASP 싱글 코어로 ASP.Net 코어에서 HTTPClient를 사용하는 가장 좋은 방법

문서 및 여러 기사에 따르면 클래스는 응용 프로그램의 수명 동안 한 번만 가장 인스턴스화되고 여러 요청에 대해 공유됩니다. 불행히도 코어에서 올바르게 수행하는 방법의 예제를 찾을 수 없으므로 다음 솔루션을 생각해 냈습니다.

나의 특별한 생각은 응용 프로그램에서 사용할 수있는 2 개의 HttpClient 싱글 톤을 가지고 있기 때문에 두 개의 다른 종단점 (비즈니스 논리와 API 기반 ImageServer 용 APIServer가 있어야 함)을 사용해야한다는 것입니다. 다음과 같이

은 내가 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 클래스에서 다음과 같이 이것을 내 종속성 컨테이너에 삽입 할 준비가되었습니다.

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

이제 내 컨트롤러에서 이것을 사용하도록 설정되었습니다.
먼저, 나는 의존성을 도입했습니다. 이렇게하기 위해 private 클래스 속성을 생성 한 다음 생성자 시그니처에 추가하고 들어오는 객체를 로컬 클래스 속성에 할당하여 마칩니다.

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

마지막으로 이제 공장을 사용하여 htppclient를 요청하고 전화를 걸 수 있습니다. 다음은 httpclients 팩터를 사용하는 이미지 서버에서 이미지를 요청하는 예제입니다.

[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에 따르면 : '이 유형의 공용 static (Visual Basic Basic에서는 Shared) 멤버는 스레드로부터 안전합니다.')
  2. 이 설정은 많은 개별 연결을 열지 않고도 과중한 부하를 처리 할 수 ​​있습니까?
  3. 'Singleton HttpClient?'에서 설명한 DNS 문제를 처리하기 위해 ASP.Net 코어에서해야 할 일은 무엇입니까? 이 심각한 행동과 수정 방법에주의하십시오. '위치 : http://byterot.blogspot.be/2016/07/singleton-httpclient-dns.html
  4. 다른 개선 사항이나 제안이 있습니까?
+0

흥미로운 접근 방식을 업로드하지만 난 공장 패턴을 생각할가 타격을 . 나는 인증을 필요로하는 API 또는 API 케이스가 Open API인지를 알아 내려고 노력해야 할까? 다른 토큰이 필요한 다른 요청에 대해 어떻게 처리 할 것입니까? –

+0

@MuqeetKhan 귀하의 질문에 대한 제 대답은 예상보다 오래 걸렸습니다. 그래서 아래 예제를 통해 찾으십시오. – Laobu

+0

두 개의'HttpClient' 인스턴스가 필요 없습니다. 싱글 톤을 등록하고 사용하십시오. DNS 문제는 여전히 존재합니다. sidenote로서'Factory' 클래스 **는 객체 인스턴스를 생성합니다 **. Factory 패턴에 대한 자세한 내용은 [여기] (https://msdn.microsoft.com/en-us/library/ee817667.aspx)를 참조하십시오. – gldraphael

답변

0

httpclient 요청으로 인증을 사용하는 것과 관련하여 @MuqeetKhan의 질문에 대한 대답으로.

첫 번째로, DI 및 팩토리를 사용하려는 나의 동기는 내 응용 프로그램을 다른 API와 여러 API로 쉽게 확장 할 수있게하고 내 코드 전체에 쉽게 액세스 할 수있게하는 것이 었습니다. 그것은 여러 번 재사용 할 수 있기를 희망하는 템플릿입니다.

위의 원래 질문에서 설명한 내 'GetUserPicture'컨트롤러의 경우, 실제로 단순화 이유로 인증이 제거되었습니다. 정직하게 그러나 나는 이미지 서버에서 단순히 이미지를 검색하기 위해 필요하다면 아직도 의심 스럽다. 어쨌든, 다른 컨트롤러에서 나는 확실히 그것을 필요로합니다 ...

인증 서버로 Identityserver4를 구현했습니다. 이것은 ASP Identity 위에 인증을 제공합니다. (이 경우 역할을 사용하여) 인증을 위해 IClaimsTransformer를 MVC 및 API 프로젝트에 구현했습니다 (자세한 내용은 여기 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가 누락되었습니다. 나는 어떻게해서든지 공장에 그것을 추가 할 수 있기를 바랍니다. 일단 구현하면 업데이트 할게.

항상 그렇듯이 모든 의견을 환영합니다.

다음은 인증을 사용하여 이미지 서버에 이미지를 업로드하는 예입니다 (사용자가 로그인하고 역할 관리자가 있어야 함).

'UploadUserPicture'호출 내 MVC 컨트롤러 :

[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를 서비스로 정적 메서드를 가지고

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