Skip to content

Commit

Permalink
refactor RestClient codes
Browse files Browse the repository at this point in the history
  • Loading branch information
terencefan committed Sep 11, 2024
1 parent 2dff80e commit c7498d1
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 267 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
using System.Threading.Tasks;

using Azure.Core;

using Azure.Core.Serialization;
using Microsoft.Azure.SignalR.Common;

using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -168,8 +168,9 @@ internal async Task UpdateAccessKeyAsync(CancellationToken ctoken = default)
private async Task GetAccessKeyInternalAsync(string accessToken, CancellationToken ctoken = default)
{
var api = new RestApiEndpoint(GetAccessKeyUrl, accessToken);

await new RestClient().SendAsync(
var builder = new JsonPayloadContentBuilder(new JsonObjectSerializer());
var client = new RestClient(HttpClientFactory.Instance, builder);
await client.SendAsync(
api,
HttpMethod.Get,
handleExpectedResponseAsync: HandleHttpResponseAsync,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;

using Microsoft.Extensions.Primitives;

namespace Microsoft.Azure.SignalR;

internal class RestApiEndpoint
{
public string Audience { get; }

public string Token { get; }

public IDictionary<string, StringValues> Query { get; set; }

public RestApiEndpoint(string endpoint, string token)
{
Audience = endpoint;
Token = token;
}
}
192 changes: 192 additions & 0 deletions src/Microsoft.Azure.SignalR.Common/Utilities/Rest/RestClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.SignalR.Common;
using Microsoft.Extensions.Primitives;

#nullable enable

namespace Microsoft.Azure.SignalR;

internal class RestClient
{
private readonly IHttpClientFactory _httpClientFactory;

private readonly IPayloadContentBuilder _payloadContentBuilder;

public RestClient(IHttpClientFactory httpClientFactory, IPayloadContentBuilder contentBuilder)
{
_httpClientFactory = httpClientFactory;
_payloadContentBuilder = contentBuilder;
}

public Task SendAsync(RestApiEndpoint api,
HttpMethod httpMethod,
string? methodName = null,
object[]? args = null,
Func<HttpResponseMessage, bool>? handleExpectedResponse = null,
CancellationToken cancellationToken = default)
{
return handleExpectedResponse == null
? SendAsync(api, httpMethod, methodName, args, handleExpectedResponseAsync: null, cancellationToken)
: SendAsync(api, httpMethod, methodName, args, response => Task.FromResult(handleExpectedResponse(response)), cancellationToken);
}

public Task SendAsync(RestApiEndpoint api,
HttpMethod httpMethod,
string? methodName = null,
object[]? args = null,
Func<HttpResponseMessage, Task<bool>>? handleExpectedResponseAsync = null,
CancellationToken cancellationToken = default)
{
return SendAsyncCore(Constants.HttpClientNames.UserDefault,
api,
httpMethod,
methodName,
args,
handleExpectedResponseAsync,
cancellationToken);
}

public Task SendWithRetryAsync(RestApiEndpoint api,
HttpMethod httpMethod,
string? methodName = null,
object[]? args = null,
Func<HttpResponseMessage, bool>? handleExpectedResponse = null,
CancellationToken cancellationToken = default)
{
return SendAsyncCore(Constants.HttpClientNames.Resilient,
api,
httpMethod,
methodName,
args,
handleExpectedResponse == null ? null : response => Task.FromResult(handleExpectedResponse(response)),
cancellationToken);
}

public Task SendMessageWithRetryAsync(RestApiEndpoint api,
HttpMethod httpMethod,
string? methodName = null,
object[]? args = null,
Func<HttpResponseMessage, bool>? handleExpectedResponse = null,
CancellationToken cancellationToken = default)
{
return SendAsyncCore(Constants.HttpClientNames.MessageResilient,
api,
httpMethod,
methodName,
args,
handleExpectedResponse == null ? null : response => Task.FromResult(handleExpectedResponse(response)),
cancellationToken);
}

private static Uri GetUri(string url, IDictionary<string, StringValues>? query)
{
if (query == null || query.Count == 0)
{
return new Uri(url);
}
var builder = new UriBuilder(url);
var sb = new StringBuilder(builder.Query);
if (sb.Length == 1 && sb[0] == '?')
{
sb.Clear();
}
else if (sb.Length > 0 && sb[0] != '?')
{
sb.Insert(0, '?');
}
foreach (var item in query)
{
foreach (var value in item.Value)
{
sb.Append(sb.Length > 0 ? '&' : '?');
sb.Append(Uri.EscapeDataString(item.Key));
sb.Append('=');
sb.Append(Uri.EscapeDataString(value!));
}
}
builder.Query = sb.ToString();
return builder.Uri;
}

private async Task ThrowExceptionOnResponseFailureAsync(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
return;
}

var detail = await response.Content.ReadAsStringAsync();

#if NET5_0_OR_GREATER
var innerException = new HttpRequestException(
$"Response status code does not indicate success: {(int)response.StatusCode} ({response.ReasonPhrase})", null, response.StatusCode);
#else
var innerException = new HttpRequestException(
$"Response status code does not indicate success: {(int)response.StatusCode} ({response.ReasonPhrase})");
#endif
throw response.StatusCode switch
{
HttpStatusCode.BadRequest => new AzureSignalRInvalidArgumentException(response.RequestMessage?.RequestUri?.ToString(), innerException, detail),
HttpStatusCode.Unauthorized => new AzureSignalRUnauthorizedException(response.RequestMessage?.RequestUri?.ToString(), innerException),
HttpStatusCode.NotFound => new AzureSignalRInaccessibleEndpointException(response.RequestMessage?.RequestUri?.ToString(), innerException),
_ => new AzureSignalRRuntimeException(response.RequestMessage?.RequestUri?.ToString(), innerException),
};
}

private async Task SendAsyncCore(
string httpClientName,
RestApiEndpoint api,
HttpMethod httpMethod,
string? methodName = null,
object[]? args = null,
Func<HttpResponseMessage, Task<bool>>? handleExpectedResponseAsync = null,
CancellationToken cancellationToken = default)
{
using var httpClient = _httpClientFactory.CreateClient(httpClientName);
using var request = BuildRequest(api, httpMethod, methodName, args);

try
{
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
if (handleExpectedResponseAsync == null)
{
await ThrowExceptionOnResponseFailureAsync(response);
}
else
{
if (!await handleExpectedResponseAsync(response))
{
await ThrowExceptionOnResponseFailureAsync(response);
}
}
}
catch (HttpRequestException ex)
{
throw new AzureSignalRException($"An error happened when making request to {request.RequestUri}", ex);
}
}

private HttpRequestMessage BuildRequest(RestApiEndpoint api, HttpMethod httpMethod, string? methodName = null, object[]? args = null)
{
var payload = httpMethod == HttpMethod.Post ? new PayloadMessage { Target = methodName, Arguments = args } : null;
return GenerateHttpRequest(api.Audience, api.Query, httpMethod, payload, api.Token);
}

private HttpRequestMessage GenerateHttpRequest(string url, IDictionary<string, StringValues> query, HttpMethod httpMethod, PayloadMessage? payload, string tokenString)
{
var request = new HttpRequestMessage(httpMethod, GetUri(url, query));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenString);
request.Content = _payloadContentBuilder.Build(payload);
return request;
}
}

This file was deleted.

24 changes: 0 additions & 24 deletions src/Microsoft.Azure.SignalR.Common/Utilities/RestApiEndpoint.cs

This file was deleted.

Loading

0 comments on commit c7498d1

Please sign in to comment.