Skip to content

Commit

Permalink
14th release (#12)
Browse files Browse the repository at this point in the history
* Improve messages of deserialization errors

* Add HttpRequestScope class

* Add 'With' prefix to extension methods modifying retry strategies

* Update version and README

---------

Co-authored-by: Kambiz Khojasteh <[email protected]>
  • Loading branch information
kampute and Khojasteh authored May 11, 2024
1 parent d6af688 commit ee8f935
Show file tree
Hide file tree
Showing 13 changed files with 346 additions and 52 deletions.
39 changes: 26 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,36 +111,49 @@ var data = await client.GetAsync<MyModel>("https://api.example.com/resource");

### Scoped Request Headers

In addition to setting default request headers that apply to all requests, you can also define headers for a specific set of requests using a scope. This allows
temporary changes to headers that override the default settings within a defined context, which can be essential for handling varying endpoint requirements or
testing scenarios.
In addition to setting default request headers that apply to all requests, you can define headers for a specific set of requests using a scoped approach. This feature
allows for temporary modifications to headers that override default settings within a defined context. This is particularly useful for handling varying endpoint requirements
or for testing scenarios.

The example below demonstrates how to temporarily override the `Accept` header for a series of requests, ensuring that all requests within the scope explicitly
request a specific media type.
Below is an example that demonstrates how to temporarily override the `Accept` header for a series of requests, ensuring that all requests within the scope explicitly request
a specific media type.

```csharp
using Kampute.HttpClient;

// Create a new instance of the HttpRestClient.
using var client = new HttpRestClient();

string csv;

// Begin a scoped block where the 'Accept' header is set to 'text/csv'.
// All HTTP requests within this using block will include this 'Accept' header.
using (client.BeginHeaderScope(new Dictionary<string, string> { ["Accept"] = MediaTypeNames.Text.Csv }))
{
// Perform a GET request to retrieve data as CSV. The 'Accept' header for this request
// will be 'text/csv', as specified by the scoped header.
await client.GetAsStringAsync("https://api.example.com/resource/1/csv");

// Perform another GET request within the same scope. The 'Accept' header remains
// consistent with the scoped setting, ensuring both requests expect CSV responses.
await client.GetAsStringAsync("https://api.example.com/resource/2/csv");
csv = await client.GetAsStringAsync("https://api.example.com/resource/csv");
}
```

In addition to headers, it is also possible to scope request properties. This feature is particularly useful in scenarios where you need to maintain state or
context-specific information temporarily during a series of HTTP operations. Scoped properties are managed similarly to scoped headers, enabling developers to
define temporary data attached to requests that are automatically cleared once the scope is exited.
Alternatively, you can use the `WithScope` extension method to simplify the code as follows:

```csharp
using Kampute.HttpClient;

using var client = new HttpRestClient();

var csv = await client
.WithScope()
.SetHeader("Accept", MediaTypeNames.Text.Csv)
.PerformAsync(c => c.GetAsStringAsync("https://api.example.com/resource/csv"));
```

### Scoped Request Properties

Similar to headers, you can also scope request properties. This capability is invaluable in scenarios where you need to maintain state or context-specific information temporarily
during a series of HTTP operations. Scoped properties work similarly to scoped headers, allowing developers to define temporary data attached to requests that are automatically
cleared once the scope is exited. This feature enhances the adaptability of your HTTP interactions, especially in complex or state-dependent communication scenarios.

### Custom Retry Strategies

Expand Down
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.2.1</Version>
<Version>2.3.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.2.1</Version>
<Version>2.3.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.2.1</Version>
<Version>2.3.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.2.1</Version>
<Version>2.3.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2024 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down
28 changes: 14 additions & 14 deletions src/Kampute.HttpClient/BackoffStrategies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public static class BackoffStrategies
/// </remarks>
public static IHttpBackoffProvider Once(TimeSpan delay)
{
return new UniformStrategy(delay).MaxAttempts(1).ToBackoffStrategy();
return new UniformStrategy(delay).WithMaxAttempts(1).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -90,7 +90,7 @@ public static IHttpBackoffProvider Once(TimeSpan delay)
/// </remarks>
public static IHttpBackoffProvider Once(DateTimeOffset after)
{
return new UniformStrategy(after - DateTimeOffset.UtcNow).MaxAttempts(1).ToBackoffStrategy();
return new UniformStrategy(after - DateTimeOffset.UtcNow).WithMaxAttempts(1).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -106,7 +106,7 @@ public static IHttpBackoffProvider Once(DateTimeOffset after)
/// </remarks>
public static IHttpBackoffProvider Uniform(uint maxAttempts, TimeSpan delay)
{
return new UniformStrategy(delay).MaxAttempts(maxAttempts).ToBackoffStrategy();
return new UniformStrategy(delay).WithMaxAttempts(maxAttempts).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -120,7 +120,7 @@ public static IHttpBackoffProvider Uniform(uint maxAttempts, TimeSpan delay)
/// </remarks>
public static IHttpBackoffProvider Uniform(TimeSpan timeout, TimeSpan delay)
{
return new UniformStrategy(delay).Timeout(timeout).ToBackoffStrategy();
return new UniformStrategy(delay).WithTimeout(timeout).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -137,7 +137,7 @@ public static IHttpBackoffProvider Uniform(TimeSpan timeout, TimeSpan delay)
/// </remarks>
public static IHttpBackoffProvider Linear(uint maxAttempts, TimeSpan initialDelay, TimeSpan delayStep)
{
return new LinearStrategy(initialDelay, delayStep).MaxAttempts(maxAttempts).ToBackoffStrategy();
return new LinearStrategy(initialDelay, delayStep).WithMaxAttempts(maxAttempts).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -153,7 +153,7 @@ public static IHttpBackoffProvider Linear(uint maxAttempts, TimeSpan initialDela
/// </remarks>
public static IHttpBackoffProvider Linear(TimeSpan timeout, TimeSpan initialDelay, TimeSpan delayStep)
{
return new LinearStrategy(initialDelay, delayStep).Timeout(timeout).ToBackoffStrategy();
return new LinearStrategy(initialDelay, delayStep).WithTimeout(timeout).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -169,7 +169,7 @@ public static IHttpBackoffProvider Linear(TimeSpan timeout, TimeSpan initialDela
/// </remarks>
public static IHttpBackoffProvider Linear(uint maxAttempts, TimeSpan initialDelay)
{
return new LinearStrategy(initialDelay).MaxAttempts(maxAttempts).ToBackoffStrategy();
return new LinearStrategy(initialDelay).WithMaxAttempts(maxAttempts).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -184,7 +184,7 @@ public static IHttpBackoffProvider Linear(uint maxAttempts, TimeSpan initialDela
/// </remarks>
public static IHttpBackoffProvider Linear(TimeSpan timeout, TimeSpan initialDelay)
{
return new LinearStrategy(initialDelay).Timeout(timeout).ToBackoffStrategy();
return new LinearStrategy(initialDelay).WithTimeout(timeout).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -202,7 +202,7 @@ public static IHttpBackoffProvider Linear(TimeSpan timeout, TimeSpan initialDela
/// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="rate"/> is less than 1.</exception>
public static IHttpBackoffProvider Exponential(uint maxAttempts, TimeSpan initialDelay, double rate = 2.0)
{
return new ExponentialStrategy(initialDelay, rate).MaxAttempts(maxAttempts).ToBackoffStrategy();
return new ExponentialStrategy(initialDelay, rate).WithMaxAttempts(maxAttempts).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -220,7 +220,7 @@ public static IHttpBackoffProvider Exponential(uint maxAttempts, TimeSpan initia
/// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="rate"/> is less than 1.</exception>
public static IHttpBackoffProvider Exponential(TimeSpan timeout, TimeSpan initialDelay, double rate = 2.0)
{
return new ExponentialStrategy(initialDelay, rate).Timeout(timeout).ToBackoffStrategy();
return new ExponentialStrategy(initialDelay, rate).WithTimeout(timeout).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -237,7 +237,7 @@ public static IHttpBackoffProvider Exponential(TimeSpan timeout, TimeSpan initia
/// </remarks>
public static IHttpBackoffProvider Fibonacci(uint maxAttempts, TimeSpan initialDelay, TimeSpan delayStep)
{
return new FibonacciStrategy(initialDelay, delayStep).MaxAttempts(maxAttempts).ToBackoffStrategy();
return new FibonacciStrategy(initialDelay, delayStep).WithMaxAttempts(maxAttempts).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -253,7 +253,7 @@ public static IHttpBackoffProvider Fibonacci(uint maxAttempts, TimeSpan initialD
/// </remarks>
public static IHttpBackoffProvider Fibonacci(TimeSpan timeout, TimeSpan initialDelay, TimeSpan delayStep)
{
return new FibonacciStrategy(initialDelay, delayStep).Timeout(timeout).ToBackoffStrategy();
return new FibonacciStrategy(initialDelay, delayStep).WithTimeout(timeout).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -269,7 +269,7 @@ public static IHttpBackoffProvider Fibonacci(TimeSpan timeout, TimeSpan initialD
/// </remarks>
public static IHttpBackoffProvider Fibonacci(uint maxAttempts, TimeSpan initialDelay)
{
return new FibonacciStrategy(initialDelay).MaxAttempts(maxAttempts).ToBackoffStrategy();
return new FibonacciStrategy(initialDelay).WithMaxAttempts(maxAttempts).ToBackoffStrategy();
}

/// <summary>
Expand All @@ -284,7 +284,7 @@ public static IHttpBackoffProvider Fibonacci(uint maxAttempts, TimeSpan initialD
/// </remarks>
public static IHttpBackoffProvider Fibonacci(TimeSpan timeout, TimeSpan initialDelay)
{
return new FibonacciStrategy(initialDelay).Timeout(timeout).ToBackoffStrategy();
return new FibonacciStrategy(initialDelay).WithTimeout(timeout).ToBackoffStrategy();
}

/// <summary>
Expand Down
147 changes: 147 additions & 0 deletions src/Kampute.HttpClient/HttpRequestScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
namespace Kampute.HttpClient
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

/// <summary>
/// Represents a scope of properties and headers that can be used for <see cref="HttpRestClient"/> requests.
/// </summary>
public sealed class HttpRequestScope
{
private Dictionary<string, string?>? _headers;
private Dictionary<string, object?>? _properties;

/// <summary>
/// Initializes a new instance of the <see cref="HttpRequestScope"/> class.
/// </summary>
/// <param name="client">The <see cref="HttpRestClient"/> associated with this scope.</param>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="client"/> argument is <c>null</c>>.</exception>
public HttpRequestScope(HttpRestClient client)
{
Client = client ?? throw new ArgumentNullException(nameof(client));
}

/// <summary>
/// Gets the <see cref="HttpRestClient"/> associated with this scope.
/// </summary>
/// <value>The <see cref="HttpRestClient"/> that is used to send HTTP requests within this scope.</value>
public HttpRestClient Client { get; }

/// <summary>
/// Gets the collection of headers that are configured to be applied to the HTTP requests sent within this scope.
/// </summary>
/// <value>
/// The read-only collection of key-value pairs representing the headers to be applied to the HTTP requests sent within this scope.
/// </value>
public IReadOnlyCollection<KeyValuePair<string, string?>> Headers => _headers ?? [];

/// <summary>
/// Gets the collection of properties that are configured to be applied to the HTTP requests sent within this scope.
/// </summary>
/// <value>
/// The read-only collection of key-value pairs representing the properties to be applied to the HTTP requests sent within this scope.
/// </value>
public IReadOnlyCollection<KeyValuePair<string, object?>> Properties => _properties ?? [];

/// <summary>
/// Specifies that a header should be used with the specified value for requests sent within this scope.
/// </summary>
/// <param name="name">The name of the header.</param>
/// <param name="value">The value of the header.</param>
/// <returns>The same <see cref="HttpRequestScope"/> instance for fluent chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="name"/> argument is <c>null</c>>.</exception>
public HttpRequestScope SetHeader(string name, string value)
{
if (name is null)
throw new ArgumentNullException(nameof(name));

_headers ??= [];
_headers[name] = value;
return this;
}

/// <summary>
/// Specifies that a header should be removed from requests sent within this scope.
/// </summary>
/// <param name="name">The header name to remove.</param>
/// <returns>The same <see cref="HttpRequestScope"/> instance for fluent chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="name"/> argument is <c>null</c>>.</exception>
public HttpRequestScope UnsetHeader(string name)
{
if (name is null)
throw new ArgumentNullException(nameof(name));

_headers ??= [];
_headers[name] = null;
return this;
}

/// <summary>
/// Specifies that a property should be used with the specified value for requests sent within this scope.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <param name="value">The value of the property.</param>
/// <returns>The same <see cref="HttpRequestScope"/> instance for fluent chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="name"/> argument is <c>null</c>.</exception>
public HttpRequestScope SetProperty(string name, object value)
{
if (name is null)
throw new ArgumentNullException(nameof(name));

_properties ??= [];
_properties[name] = value;
return this;
}

/// <summary>
/// Specifies that a property should be removed from requests sent within this scope.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <returns>The same <see cref="HttpRequestScope"/> instance for fluent chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="name"/> argument is <c>null</c>>.</exception>
public HttpRequestScope UnsetProperty(string name)
{
if (name is null)
throw new ArgumentNullException(nameof(name));

_properties ??= [];
_properties[name] = null;
return this;
}

/// <summary>
/// Executes a task within the configured scope, applying all set properties and headers to requests made by the client during the execution of the task.
/// </summary>
/// <param name="scopedAction">The asynchronous action to execute, which involves HTTP requests that will include the configured properties and headers.</param>
/// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="scopedAction"/> is <c>null</c>>.</exception>
public async Task PerformAsync(Func<HttpRestClient, Task> scopedAction)
{
if (scopedAction is null)
throw new ArgumentNullException(nameof(scopedAction));

using var propertyScope = _properties is not null ? Client.BeginPropertyScope(_properties) : null;
using var headerScope = _headers is not null ? Client.BeginHeaderScope(_headers) : null;
await scopedAction(Client);
}

/// <summary>
/// Executes a task within the configured scope, applying all set properties and headers to requests made by the client during the execution of the task, and
/// returns a result of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of the result returned by the scoped action.</typeparam>
/// <param name="scopedFunction">The asynchronous function to execute, which involves HTTP requests that will include the configured properties and headers.</param>
/// <returns>A task representing the asynchronous operation with a result of type <typeparamref name="T"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="scopedFunction"/> is <c>null</c>>.</exception>
public async Task<T> PerformAsync<T>(Func<HttpRestClient, Task<T>> scopedFunction)
{
if (scopedFunction is null)
throw new ArgumentNullException(nameof(scopedFunction));

using var propertyScope = _properties is not null ? Client.BeginPropertyScope(_properties) : null;
using var headerScope = _headers is not null ? Client.BeginHeaderScope(_headers) : null;
return await scopedFunction(Client);
}
}
}
Loading

0 comments on commit ee8f935

Please sign in to comment.