Implemented PATCH as defined in .NET 6.0 for .NET Standard 2.0

This commit is contained in:
2024-10-31 13:01:06 +01:00
parent c6eb6ba05e
commit 25c8e9b5b0
8 changed files with 140 additions and 80 deletions

View File

@@ -20,7 +20,7 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
[TestClass] [TestClass]
public class DeleteAsyncTest public class DeleteAsyncTest
{ {
private const string _baseUrl = "http://localhost/api/v4/"; private const string BaseUrl = "http://localhost/api/v4/";
private HttpMessageHandlerMock _httpHandlerMock; private HttpMessageHandlerMock _httpHandlerMock;
private Mock<ClientOptions> _clientOptionsMock; private Mock<ClientOptions> _clientOptionsMock;
@@ -37,7 +37,7 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
.Setup(a => a.AddHeader(It.IsAny<HttpClient>())) .Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token")); .Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token"));
_clientOptionsMock.Setup(o => o.BaseUrl).Returns(_baseUrl); _clientOptionsMock.Setup(o => o.BaseUrl).Returns(BaseUrl);
_clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60)); _clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60));
_clientOptionsMock.Setup(o => o.MaxRetries).Returns(2); _clientOptionsMock.Setup(o => o.MaxRetries).Returns(2);
_clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>()); _clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>());
@@ -275,8 +275,8 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
{ {
var httpClient = new HttpClient(_httpHandlerMock.Mock.Object) var httpClient = new HttpClient(_httpHandlerMock.Mock.Object)
{ {
BaseAddress = new Uri(_baseUrl),
Timeout = _clientOptionsMock.Object.Timeout, Timeout = _clientOptionsMock.Object.Timeout,
BaseAddress = new Uri(BaseUrl),
}; };
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0")); httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0"));

View File

@@ -20,7 +20,7 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
[TestClass] [TestClass]
public class GetAsyncTest public class GetAsyncTest
{ {
private const string _baseUrl = "http://localhost/api/v4/"; private const string BaseUrl = "http://localhost/api/v4/";
private HttpMessageHandlerMock _httpHandlerMock; private HttpMessageHandlerMock _httpHandlerMock;
private Mock<ClientOptions> _clientOptionsMock; private Mock<ClientOptions> _clientOptionsMock;
@@ -37,7 +37,7 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
.Setup(a => a.AddHeader(It.IsAny<HttpClient>())) .Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token")); .Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token"));
_clientOptionsMock.Setup(o => o.BaseUrl).Returns(_baseUrl); _clientOptionsMock.Setup(o => o.BaseUrl).Returns(BaseUrl);
_clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60)); _clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60));
_clientOptionsMock.Setup(o => o.MaxRetries).Returns(2); _clientOptionsMock.Setup(o => o.MaxRetries).Returns(2);
_clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>()); _clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>());
@@ -168,6 +168,27 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
} }
} }
[DataTestMethod]
[DataRow(HttpStatusCode.Unauthorized)]
[DataRow(HttpStatusCode.Forbidden)]
[ExpectedException(typeof(CloudflareException))]
public async Task ShouldThrowCloudflareExceptionOnStatusCodeWhenDeserializeFails(HttpStatusCode statusCode)
{
// Arrange
_httpHandlerMock?.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent("", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
// Act
await client.GetAsync<TestClass>("foo");
// Assert - CloudflareException
}
[TestMethod] [TestMethod]
public async Task ShouldReturnPlainText() public async Task ShouldReturnPlainText()
{ {
@@ -250,6 +271,25 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
await client.GetAsync<TestClass>("some-path"); await client.GetAsync<TestClass>("some-path");
} }
[TestMethod]
[ExpectedException(typeof(CloudflareException))]
public async Task ShouldThrowCloudflareExceptionWhenDeserializeFails()
{
// Arrange
_httpHandlerMock?.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
// Act
await client.GetAsync<TestClass>("foo");
// Assert - CloudflareException
}
private void VerifyDefaults() private void VerifyDefaults()
{ {
_authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once); _authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once);
@@ -275,8 +315,8 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
{ {
var httpClient = new HttpClient(_httpHandlerMock.Mock.Object) var httpClient = new HttpClient(_httpHandlerMock.Mock.Object)
{ {
BaseAddress = new Uri(_baseUrl),
Timeout = _clientOptionsMock.Object.Timeout, Timeout = _clientOptionsMock.Object.Timeout,
BaseAddress = new Uri(BaseUrl),
}; };
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0")); httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0"));

View File

@@ -20,7 +20,7 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
[TestClass] [TestClass]
public class PatchAsyncTest public class PatchAsyncTest
{ {
private const string _baseUrl = "https://localhost/api/v4/"; private const string BaseUrl = "https://localhost/api/v4/";
private HttpMessageHandlerMock _httpHandlerMock; private HttpMessageHandlerMock _httpHandlerMock;
private Mock<ClientOptions> _clientOptionsMock; private Mock<ClientOptions> _clientOptionsMock;
@@ -39,7 +39,7 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
.Setup(a => a.AddHeader(It.IsAny<HttpClient>())) .Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token")); .Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token"));
_clientOptionsMock.Setup(o => o.BaseUrl).Returns(_baseUrl); _clientOptionsMock.Setup(o => o.BaseUrl).Returns(BaseUrl);
_clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60)); _clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60));
_clientOptionsMock.Setup(o => o.MaxRetries).Returns(2); _clientOptionsMock.Setup(o => o.MaxRetries).Returns(2);
_clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>()); _clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>());
@@ -335,8 +335,8 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
{ {
var httpClient = new HttpClient(_httpHandlerMock.Mock.Object) var httpClient = new HttpClient(_httpHandlerMock.Mock.Object)
{ {
BaseAddress = new Uri(_baseUrl),
Timeout = _clientOptionsMock.Object.Timeout, Timeout = _clientOptionsMock.Object.Timeout,
BaseAddress = new Uri(BaseUrl),
}; };
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0")); httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0"));

View File

@@ -20,7 +20,7 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
[TestClass] [TestClass]
public class PostAsyncTest public class PostAsyncTest
{ {
private const string _baseUrl = "https://localhost/api/v4/"; private const string BaseUrl = "https://localhost/api/v4/";
private HttpMessageHandlerMock _httpHandlerMock; private HttpMessageHandlerMock _httpHandlerMock;
private Mock<ClientOptions> _clientOptionsMock; private Mock<ClientOptions> _clientOptionsMock;
@@ -39,7 +39,7 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
.Setup(a => a.AddHeader(It.IsAny<HttpClient>())) .Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token")); .Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token"));
_clientOptionsMock.Setup(o => o.BaseUrl).Returns(_baseUrl); _clientOptionsMock.Setup(o => o.BaseUrl).Returns(BaseUrl);
_clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60)); _clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60));
_clientOptionsMock.Setup(o => o.MaxRetries).Returns(2); _clientOptionsMock.Setup(o => o.MaxRetries).Returns(2);
_clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>()); _clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>());
@@ -440,8 +440,8 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
{ {
var httpClient = new HttpClient(_httpHandlerMock.Mock.Object) var httpClient = new HttpClient(_httpHandlerMock.Mock.Object)
{ {
BaseAddress = new Uri(_baseUrl),
Timeout = _clientOptionsMock.Object.Timeout, Timeout = _clientOptionsMock.Object.Timeout,
BaseAddress = new Uri(BaseUrl),
}; };
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0")); httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0"));

View File

@@ -20,7 +20,7 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
[TestClass] [TestClass]
public class PutAsyncTest public class PutAsyncTest
{ {
private const string _baseUrl = "https://localhost/api/v4/"; private const string BaseUrl = "https://localhost/api/v4/";
private HttpMessageHandlerMock _httpHandlerMock; private HttpMessageHandlerMock _httpHandlerMock;
private Mock<ClientOptions> _clientOptionsMock; private Mock<ClientOptions> _clientOptionsMock;
@@ -39,7 +39,7 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
.Setup(a => a.AddHeader(It.IsAny<HttpClient>())) .Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token")); .Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token"));
_clientOptionsMock.Setup(o => o.BaseUrl).Returns(_baseUrl); _clientOptionsMock.Setup(o => o.BaseUrl).Returns(BaseUrl);
_clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60)); _clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60));
_clientOptionsMock.Setup(o => o.MaxRetries).Returns(2); _clientOptionsMock.Setup(o => o.MaxRetries).Returns(2);
_clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>()); _clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>());
@@ -383,8 +383,8 @@ namespace Cloudflare.Core.Tests.CloudflareClientTests
{ {
var httpClient = new HttpClient(_httpHandlerMock.Mock.Object) var httpClient = new HttpClient(_httpHandlerMock.Mock.Object)
{ {
BaseAddress = new Uri(_baseUrl),
Timeout = _clientOptionsMock.Object.Timeout, Timeout = _clientOptionsMock.Object.Timeout,
BaseAddress = new Uri(BaseUrl),
}; };
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0")); httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0"));

View File

@@ -17,9 +17,9 @@ namespace AMWD.Net.Api.Cloudflare
/// <summary> /// <summary>
/// Implements the Core of the Cloudflare API client. /// Implements the Core of the Cloudflare API client.
/// </summary> /// </summary>
public partial class CloudflareClient : ICloudflareClient, IDisposable public class CloudflareClient : ICloudflareClient, IDisposable
{ {
private static readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings private static readonly JsonSerializerSettings _jsonSerializerSettings = new()
{ {
Culture = CultureInfo.InvariantCulture, Culture = CultureInfo.InvariantCulture,
Formatting = Formatting.None, Formatting = Formatting.None,
@@ -100,23 +100,9 @@ namespace AMWD.Net.Api.Cloudflare
ValidateRequestPath(requestPath); ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath, queryFilter); string requestUrl = BuildRequestUrl(requestPath, queryFilter);
var httpContent = ConvertRequest(request);
HttpContent httpRequestContent; var response = await _httpClient.PostAsync(requestUrl, httpContent, cancellationToken).ConfigureAwait(false);
if (request == null)
{
httpRequestContent = null;
}
else if (request is HttpContent httpContent)
{
httpRequestContent = httpContent;
}
else
{
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.PostAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false); return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
} }
@@ -127,23 +113,9 @@ namespace AMWD.Net.Api.Cloudflare
ValidateRequestPath(requestPath); ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath); string requestUrl = BuildRequestUrl(requestPath);
var httpContent = ConvertRequest(request);
HttpContent httpRequestContent; var response = await _httpClient.PutAsync(requestUrl, httpContent, cancellationToken).ConfigureAwait(false);
if (request == null)
{
httpRequestContent = null;
}
else if (request is HttpContent httpContent)
{
httpRequestContent = httpContent;
}
else
{
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.PutAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false); return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
} }
@@ -166,31 +138,9 @@ namespace AMWD.Net.Api.Cloudflare
ValidateRequestPath(requestPath); ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath); string requestUrl = BuildRequestUrl(requestPath);
var httpContent = ConvertRequest(request);
HttpContent httpRequestContent; var response = await _httpClient.PatchAsync(requestUrl, httpContent, cancellationToken).ConfigureAwait(false);
if (request is HttpContent httpContent)
{
httpRequestContent = httpContent;
}
else
{
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
}
#if NET6_0_OR_GREATER
var response = await _httpClient.PatchAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
#else
var httpRequestMessage = new HttpRequestMessage
{
Version = HttpVersion.Version11,
Method = new HttpMethod("PATCH"),
RequestUri = new Uri(requestUrl),
Content = httpRequestContent,
};
var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
#endif
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false); return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
} }
@@ -266,24 +216,29 @@ namespace AMWD.Net.Api.Cloudflare
return client; return client;
} }
private async Task<CloudflareResponse<TRes>> GetCloudflareResponse<TRes>(HttpResponseMessage response, CancellationToken cancellationToken) private static async Task<CloudflareResponse<TRes>> GetCloudflareResponse<TRes>(HttpResponseMessage httpResponse, CancellationToken cancellationToken)
{ {
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
string content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); string content = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
#else #else
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); string content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
#endif #endif
switch (response.StatusCode) switch (httpResponse.StatusCode)
{ {
case HttpStatusCode.Forbidden: case HttpStatusCode.Forbidden:
case HttpStatusCode.Unauthorized: case HttpStatusCode.Unauthorized:
var errorResponse = JsonConvert.DeserializeObject<CloudflareResponse<object>>(content); var errorResponse = JsonConvert.DeserializeObject<CloudflareResponse<object>>(content, _jsonSerializerSettings)
?? throw new CloudflareException("Response is not a valid Cloudflare API response.");
throw new AuthenticationException(string.Join(Environment.NewLine, errorResponse.Errors.Select(e => $"{e.Code}: {e.Message}"))); throw new AuthenticationException(string.Join(Environment.NewLine, errorResponse.Errors.Select(e => $"{e.Code}: {e.Message}")));
default: default:
try try
{ {
return JsonConvert.DeserializeObject<CloudflareResponse<TRes>>(content); var response = JsonConvert.DeserializeObject<CloudflareResponse<TRes>>(content)
?? throw new CloudflareException("Response is not a valid Cloudflare API response.");
return response;
} }
catch catch
{ {
@@ -328,5 +283,17 @@ namespace AMWD.Net.Api.Cloudflare
return $"{requestPath}?{query}"; return $"{requestPath}?{query}";
} }
private static HttpContent ConvertRequest<T>(T request)
{
if (request == null)
return null;
if (request is HttpContent httpContent)
return httpContent;
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
return new StringContent(json, Encoding.UTF8, "application/json");
}
} }
} }

View File

@@ -0,0 +1,53 @@
#if ! NET6_0_OR_GREATER
using System.Threading;
using System.Threading.Tasks;
namespace System.Net.Http
{
/// <summary>
/// Extension methods for <see cref="HttpClient"/>s.
/// </summary>
/// <remarks>
/// Copied from <see href="https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs">.NET 6 runtime / HttpClient</see>.
/// </remarks>
[Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static class HttpClientExtensions
{
private static readonly HttpMethod _httpMethodPatch = new("PATCH");
/// <summary>
/// Sends a PATCH request with a cancellation token to a Uri represented as a string as an asynchronous operation.
/// </summary>
/// <param name="client">A <see cref="HttpClient"/> instance.</param>
/// <param name="requestUri">The Uri the request is sent to.</param>
/// <param name="content">The HTTP request content sent to the server.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task<HttpResponseMessage> PatchAsync(this HttpClient client, string? requestUri, HttpContent? content, CancellationToken cancellationToken) =>
client.PatchAsync(CreateUri(requestUri), content, cancellationToken);
/// <summary>
/// Sends a PATCH request with a cancellation token as an asynchronous operation.
/// </summary>
/// <param name="client">A <see cref="HttpClient"/> instance.</param>
/// <param name="requestUri">The Uri the request is sent to.</param>
/// <param name="content">The HTTP request content sent to the server.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>The task object representing the asynchrnous operation.</returns>
public static Task<HttpResponseMessage> PatchAsync(this HttpClient client, Uri? requestUri, HttpContent? content, CancellationToken cancellationToken)
{
var request = new HttpRequestMessage(_httpMethodPatch, requestUri)
{
Version = HttpVersion.Version11,
Content = content,
};
return client.SendAsync(request, cancellationToken);
}
private static Uri? CreateUri(string? uri) =>
string.IsNullOrEmpty(uri) ? null : new Uri(uri, UriKind.RelativeOrAbsolute);
}
}
#endif

View File

@@ -35,7 +35,7 @@ namespace AMWD.Net.Api.Cloudflare
/// <summary> /// <summary>
/// The base Cloudflare response with a result. /// The base Cloudflare response with a result.
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T">The result type.</typeparam>
public class CloudflareResponse<T> : CloudflareResponse public class CloudflareResponse<T> : CloudflareResponse
{ {
/// <summary> /// <summary>