Skip to content

Commit

Permalink
Enhance functionality of scoped properties and headers
Browse files Browse the repository at this point in the history
  • Loading branch information
Khojasteh committed May 7, 2024
1 parent f4fec3d commit 7bc3d8b
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Title>Kampute.HttpClient.DataContract</Title>
<Description>This package is an extension package for Kampute.HttpClient, enhancing it to manage application/xml content types, using DataContractSerializer for serialization and deserialization of XML responses and payloads.</Description>
<Authors>Kambiz Khojasteh</Authors>
<Version>2.1.1</Version>
<Version>2.2.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2024 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down
2 changes: 1 addition & 1 deletion src/Kampute.HttpClient.Json/Kampute.HttpClient.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Title>Kampute.HttpClient.Json</Title>
<Description>This package is an extension package for Kampute.HttpClient, enhancing it to manage application/json content types, using System.Text.Json library for serialization and deserialization of JSON responses and payloads.</Description>
<Authors>Kambiz Khojasteh</Authors>
<Version>2.1.1</Version>
<Version>2.2.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2024 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Title>Kampute.HttpClient.NewtonsoftJson</Title>
<Description>This package is an extension package for Kampute.HttpClient, enhancing it to manage application/json content types, using Newtonsoft.Json library for serialization and deserialization of JSON responses and payloads.</Description>
<Authors>Kambiz Khojasteh</Authors>
<Version>2.1.1</Version>
<Version>2.2.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2024 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down
2 changes: 1 addition & 1 deletion src/Kampute.HttpClient.Xml/Kampute.HttpClient.Xml.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Title>Kampute.HttpClient.Xml</Title>
<Description>This package is an extension package for Kampute.HttpClient, enhancing it to manage application/xml content types, using XmlSerializer for serialization and deserialization of XML responses and payloads.</Description>
<Authors>Kambiz Khojasteh</Authors>
<Version>2.1.1</Version>
<Version>2.2.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2024 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down
34 changes: 31 additions & 3 deletions src/Kampute.HttpClient/ErrorHandlers/HttpError401Handler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public HttpError401Handler(Func<HttpResponseErrorContext, CancellationToken, Tas

await _lastAuthorization.TryUpdateAsync(async () =>
{
using (ctx.Client.BeginPropertyScope(new Dictionary<string, object> { [HttpRequestMessagePropertyKeys.SkipUnauthorizedHandling] = true }))
using (ctx.Client.BeginPropertyScope(AuthorizationScope.Properties))
{
return await _asyncAuthenticator(ctx, cancellationToken).ConfigureAwait(false);
}
Expand All @@ -114,10 +114,9 @@ await _lastAuthorization.TryUpdateAsync(async () =>
/// <inheritdoc/>
async Task<HttpErrorHandlerResult> IHttpErrorHandler.DecideOnRetryAsync(HttpResponseErrorContext ctx, CancellationToken cancellationToken)
{
if (ctx.Request.Properties.ContainsKey(HttpRequestMessagePropertyKeys.SkipUnauthorizedHandling))
if (ctx.Request.Properties.TryGetValue(HttpRequestMessagePropertyKeys.SkipUnauthorizedHandling, out var skip) && skip is true)
return HttpErrorHandlerResult.NoRetry;


var authorization = await AuthenticateAsync(ctx, cancellationToken).ConfigureAwait(false);
if (authorization is null)
return HttpErrorHandlerResult.NoRetry;
Expand All @@ -134,5 +133,34 @@ async Task<HttpErrorHandlerResult> IHttpErrorHandler.DecideOnRetryAsync(HttpResp
/// Releases the unmanaged resources used by the <see cref="HttpError401Handler"/> and optionally disposes of the managed resources.
/// </summary>
public void Dispose() => _lastAuthorization.Dispose();

/// <summary>
/// Provides the request properties to be set during authorization process.
/// </summary>
/// <remarks>
/// This class defines request properties that are used during the authorization process.
/// <list type="bullet">
/// <item>
/// <term><see cref="HttpRequestMessagePropertyKeys.SkipUnauthorizedHandling"/></term>
/// <description>
/// A flag indicating whether the request should skip the authorization process.
/// <para>
/// This property is set to <c>true</c> for all requests initiated by the authorization process
/// to prevent the handler from reentering itself and causing potential deadlocks.
/// </para>
/// </description>
/// </item>
/// </list>
/// </remarks>
private static class AuthorizationScope
{
/// <summary>
/// Gets the scoped properties of requests initiated by the authorization process.
/// </summary>
public static IEnumerable<KeyValuePair<string, object?>> Properties =>
[
new KeyValuePair<string, object?>(HttpRequestMessagePropertyKeys.SkipUnauthorizedHandling, true)
];
}
}
}
21 changes: 20 additions & 1 deletion src/Kampute.HttpClient/HttpRequestMessagePropertyKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace Kampute.HttpClient
{
using Kampute.HttpClient.Interfaces;
using System;
using System.Net.Http;

/// <summary>
Expand All @@ -15,40 +16,58 @@ public static class HttpRequestMessagePropertyKeys
{
/// <summary>
/// A key used to store and identify the property within an <see cref="HttpRequestMessage"/> that tracks
/// how many times the request has been cloned.
/// how many times the request has been cloned.
/// </summary>
/// <remarks>
/// The value of this property is of type <see cref="int"/>.
/// </remarks>
public const string CloneGeneration = nameof(HttpRestClient) + "." + nameof(CloneGeneration);

/// <summary>
/// A key used to store and identify the property within an <see cref="HttpRequestMessage"/> that identifies
/// the request and its clones.
/// </summary>
/// <remarks>
/// The value of this property is of type <see cref="Guid"/>.
/// </remarks>
public const string TransactionId = nameof(HttpRestClient) + "." + nameof(TransactionId);

/// <summary>
/// A key used to store and identify the property within an <see cref="HttpRequestMessage"/> that identifies
/// the type of expected .NET object in the response.
/// </summary>
/// <remarks>
/// The value of this property is of type <see cref="Type"/>.
/// </remarks>
public const string ResponseObjectType = nameof(HttpRestClient) + "." + nameof(ResponseObjectType);

/// <summary>
/// A key used to store and identify the property within an <see cref="HttpRequestMessage"/> that references
/// the <see cref="IRetryScheduler"/> instance associated with the request which is responsible for scheduling
/// the retry logic for transient failures.
/// </summary>
/// <remarks>
/// The value of this property is of type <see cref="IRetryScheduler"/>.
/// </remarks>
public const string RetryScheduler = nameof(HttpRestClient) + "." + nameof(RetryScheduler);

/// <summary>
/// A key used to store and identify the property within an <see cref="HttpRequestMessage"/> that references
/// the <see cref="IHttpErrorHandler"/> instance associated with the request, which is responsible for processing
/// and potentially recovering from errors in the response.
/// </summary>
/// <remarks>
/// The value of this property is of type <see cref="IHttpErrorHandler"/>.
/// </remarks>
public const string ErrorHandler = nameof(HttpRestClient) + "." + nameof(ErrorHandler);

/// <summary>
/// A key used to store and identify the property within an <see cref="HttpRequestMessage"/> that indicates
/// '401 Unauthorized' errors should not be automatically handled.
/// </summary>
/// <remarks>
/// The value of this property is of type <see cref="bool"/>.
/// </remarks>
public const string SkipUnauthorizedHandling = nameof(HttpRestClient) + "." + nameof(SkipUnauthorizedHandling);
}
}
85 changes: 51 additions & 34 deletions src/Kampute.HttpClient/HttpRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ private static HttpRequestHeaders CreateRequestHeaders()
private readonly HttpClient _httpClient;
private readonly IDisposable? _disposable;

private readonly Lazy<ScopedCollection<KeyValuePair<string, string>>> _scopedHeaders = new(LazyThreadSafetyMode.ExecutionAndPublication);
private readonly Lazy<ScopedCollection<KeyValuePair<string, object>>> _scopedProperties = new(LazyThreadSafetyMode.ExecutionAndPublication);
private readonly Lazy<ScopedCollection<KeyValuePair<string, string?>>> _scopedHeaders = new(LazyThreadSafetyMode.ExecutionAndPublication);
private readonly Lazy<ScopedCollection<KeyValuePair<string, object?>>> _scopedProperties = new(LazyThreadSafetyMode.ExecutionAndPublication);

private IHttpBackoffProvider _backoffStrategy = BackoffStrategies.None;
private Uri? _baseAddress;
Expand Down Expand Up @@ -242,22 +242,26 @@ public void Dispose()
}

/// <summary>
/// Begins a new scope with the specified properties.
/// Begins a new scope with the specified request properties.
/// </summary>
/// <param name="properties">The properties to include in the new scope.</param>
/// <returns>An <see cref="IDisposable"/> representing the new scope. Disposing this object will end the scope and remove the associated properties.</returns>
/// <param name="properties">The request properties to include in the new scope.</param>
/// <returns>An <see cref="IDisposable"/> representing the new scope. Disposing this object will end the scope and revert changes in the request properties.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="properties"/> is <c>null</c>.</exception>
/// <remarks>
/// <para>
/// The scope created by this method will be associated with the current <see cref="HttpRestClient"/> instance. The properties within the scope will
/// be included in the properties of all subsequent HTTP requests made by this client until the scope is disposed.
/// This method creates a scope associated with the current <see cref="HttpRestClient"/> instance to add, modify, or remove any request property in
/// subsequent requests during the lifetime of this scope. To remove a property, set its value to <c>null</c>.
/// </para>
/// <para>
/// By using a scope, you can ensure that all related requests carry the same contextual information, which can be critical for tracing, logging,
/// and maintaining state across asynchronous operations or across different components of an application.
/// Any property modifications made within this scope take precedence over the property adjustemnts by other active scopes. In case of conflicts,
/// the properties provided by this scope will override the others.
/// </para>
/// <para>
/// Upon disposing of the scope, all property adjustments are reverted, restoring the properties to their original configuration prior to the scope's
/// activation.
/// </para>
/// </remarks>
public virtual IDisposable BeginPropertyScope(IEnumerable<KeyValuePair<string, object>> properties)
public virtual IDisposable BeginPropertyScope(IEnumerable<KeyValuePair<string, object?>> properties)
{
if (properties is null)
throw new ArgumentNullException(nameof(properties));
Expand All @@ -266,21 +270,30 @@ public virtual IDisposable BeginPropertyScope(IEnumerable<KeyValuePair<string, o
}

/// <summary>
/// Begins a new scope with the specified HTTP headers.
/// Begins a new scope with the specified request headers.
/// </summary>
/// <param name="httpHeaders">The HTTP headers to include in the new scope.</param>
/// <returns>An <see cref="IDisposable"/> representing the new scope. Disposing this object will end the scope and remove the associated HTTP headers.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="httpHeaders"/> is <c>null</c>.</exception>
/// <param name="headers">The request headers to include in the new scope.</param>
/// <returns>An <see cref="IDisposable"/> representing the new scope. Disposing this object will end the scope and revert changes in the request headers.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="headers"/> is <c>null</c>.</exception>
/// <remarks>
/// The scope created by this method will be associated with the current <see cref="HttpRestClient"/> instance. The HTTP headers within the scope will
/// be included in the HTTP headers of all subsequent HTTP requests made by this client until the scope is disposed.
/// <para>
/// This method creates a scope associated with the current <see cref="HttpRestClient"/> instance to add, modify, or remove any request header in subsequent
/// requests during the lifetime of this scope. To remove a header, set its value to <c>null</c>.
/// </para>
/// <para>
/// Any header modifications made within this scope take precedence over the client's default headers and header adjustments by other active scopes. In case of
/// conflicts, the headers provided by this scope will override the others.
/// </para>
/// <para>
/// Upon disposing of the scope, all header adjustments are reverted, restoring the headers to their original configuration prior to the scope's activation.
/// </para>
/// </remarks>
public virtual IDisposable BeginHeaderScope(IEnumerable<KeyValuePair<string, string>> httpHeaders)
public virtual IDisposable BeginHeaderScope(IEnumerable<KeyValuePair<string, string?>> headers)
{
if (httpHeaders is null)
throw new ArgumentNullException(nameof(httpHeaders));
if (headers is null)
throw new ArgumentNullException(nameof(headers));

return _scopedHeaders.Value.BeginScope(httpHeaders);
return _scopedHeaders.Value.BeginScope(headers);
}

/// <summary>
Expand Down Expand Up @@ -605,9 +618,9 @@ HttpContentException Error(string message, Exception? innerException = null)
/// to ensure that context-specific modifications are respected.
/// </para>
/// <para>
/// If the underlying HTTP client's default request headers, the headers provided by the <see cref="DefaultRequestHeaders"/> property, or any scoped
/// headers do not already include an <c>Accept</c> header, it is added to align with the media types supported by the content deserializers appropriate for
/// the specified <paramref name="responseObjectType"/>. If <paramref name="responseObjectType"/> is <c>null</c>, it defaults to accepting all media types.
/// If an <c>Accept</c> header is absent in both default and scoped headers, it is added based on the media types supported by the content deserializers
/// for the specified <paramref name="responseObjectType"/>. If <paramref name="responseObjectType"/> is <c>null</c>, the header defaults to accepting all
/// media types ("*/*").
/// </para>
/// <para>
/// This method also includes scoped properties in the HTTP request message to provide additional context and facilitate easier tracking and processing
Expand All @@ -624,7 +637,7 @@ HttpContentException Error(string message, Exception? innerException = null)
/// <item>
/// <term><see cref="HttpRequestMessagePropertyKeys.ResponseObjectType"/></term>
/// <description>
/// Defines the .NET type expected in the response, if any. This metadata provides context that can improve debugging, enhance logging details,
/// Defines the .NET type (<see cref="Type"/>) expected in the response, if any. This metadata provides context that can improve debugging, enhance logging details,
/// and support error recovery strategies.
/// </description>
/// </item>
Expand All @@ -640,8 +653,10 @@ protected virtual HttpRequestMessage CreateHttpRequest(HttpMethod method, string

var requestUri = _baseAddress is null ? new Uri(uri) : new Uri(_baseAddress, uri);
var request = new HttpRequestMessage(method, requestUri);

AddRequestHeaders();
AddRequestProperties();

return request;

void AddRequestHeaders()
Expand All @@ -654,15 +669,12 @@ void AddRequestHeaders()
foreach (var header in _scopedHeaders.Value)
{
request.Headers.Remove(header.Key);
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
if (header.Value is not null)
request.Headers.Add(header.Key, header.Value);
}
}

if
(
!_httpClient.DefaultRequestHeaders.Contains(nameof(HttpRequestHeader.Accept)) &&
!request.Headers.Contains(nameof(HttpRequestHeader.Accept))
)
if (!request.Headers.Contains(nameof(HttpRequestHeader.Accept)))
{
foreach (var mediaType in ResponseDeserializers.GetAcceptableMediaTypes(responseObjectType, ResponseErrorType))
request.Headers.Accept.Add(MediaTypeHeaderValueStore.Get(mediaType));
Expand All @@ -671,14 +683,19 @@ void AddRequestHeaders()

void AddRequestProperties()
{
request.Properties[HttpRequestMessagePropertyKeys.TransactionId] = Guid.NewGuid();
request.Properties[HttpRequestMessagePropertyKeys.ResponseObjectType] = responseObjectType;

if (_scopedProperties.IsValueCreated)
{
foreach (var property in _scopedProperties.Value)
request.Properties[property.Key] = property.Value;
{
if (property.Value is not null)
request.Properties[property.Key] = property.Value;
else
request.Properties.Remove(property.Key);
}
}

request.Properties[HttpRequestMessagePropertyKeys.TransactionId] = Guid.NewGuid();
request.Properties[HttpRequestMessagePropertyKeys.ResponseObjectType] = responseObjectType;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Kampute.HttpClient/Kampute.HttpClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Title>Kampute.HttpClient</Title>
<Description>Kampute.HttpClient is a versatile and lightweight .NET library that simplifies RESTful API communication. Its core HttpRestClient class provides a streamlined approach to HTTP interactions, offering advanced features such as flexible serialization/deserialization, robust error handling, configurable backoff strategies, and detailed request-response processing. Striking a balance between simplicity and extensibility, Kampute.HttpClient empowers developers with a powerful yet easy-to-use client for seamless API integration across a wide range of .NET applications.</Description>
<Authors>Kambiz Khojasteh</Authors>
<Version>2.1.1</Version>
<Version>2.2.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2024 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down
Loading

0 comments on commit 7bc3d8b

Please sign in to comment.