Skip to content

Commit

Permalink
9th release (#7)
Browse files Browse the repository at this point in the history
* Major refactoring and code rewrite

* Improve documentation of GetAcceptableMediaTypes

* Rename a method to its original name

* Add scoped headers

* Add SharedHttpClient class

* Fix a bug related to the accept header values

* Update README file

* Add default constructor to AsyncUpdateThrottle

* Add default constructor to SharedDisposable

* Fix bug in property and header scopes

* Enable ProduceReferenceAssembly compiler option

---------

Co-authored-by: Kambiz Khojasteh <[email protected]>
  • Loading branch information
kampute and Khojasteh authored May 5, 2024
1 parent fdbc382 commit 9cdd999
Show file tree
Hide file tree
Showing 68 changed files with 2,118 additions and 1,531 deletions.
45 changes: 42 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

`Kampute.HttpClient` is a .NET library designed to simplify HTTP communication with RESTful APIs by enhancing the native `HttpClient` capabilities. Tailored for
developers seeking a potent yet flexible HTTP client for API integration within .NET applications, it combines ease of use with a wide array of functionalities
to address the complexities of web service consumption. [Explore the API documentation](https://kampute.github.io/http-client/api/Kampute.HttpClient.html) for
detailed insights.
to address the complexities of web service consumption.

[Explore the API documentation](https://kampute.github.io/http-client/api/Kampute.HttpClient.html) for detailed insights.

## Key Features

Expand All @@ -18,6 +19,11 @@ detailed insights.
Allows the integration of custom or shared `HttpClient` instances, complete with configurations for message handlers, timeouts, and advanced authentication
mechanisms to fit specific application needs.

- **Dynamic Request Customization:**
Offers the capability to define headers and properties scoped to specific request blocks, allowing for temporary changes that do not affect the global configuration.
Scoped headers and properties ensure that modifications are contextually isolated, enhancing maintainability and reducing the risk of configuration errors during
runtime.

- **Custom Error Handling and Exception Management:**
Converts HTTP response errors into detailed, meaningful exceptions, streamlining the process of interpreting API-specific errors with the aid of a customizable
error response type set through the `ResponseErrorType` property. Furthermore, it enhances flexibility in error management with the `ErrorHandlers` collection,
Expand Down Expand Up @@ -103,6 +109,39 @@ client.AcceptJson();
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.

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.

```csharp
using Kampute.HttpClient;

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

// 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");
}
```

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.

### Custom Retry Strategies

The library offers various retry strategies to manage transient failures, ensuring your application remains resilient during network instability or temporary
Expand Down Expand Up @@ -210,4 +249,4 @@ first to discuss what you would like to change.

## License

`Kampute.HttpClient` is licensed under the terms of the [MIT](LICENSE) license.
`Kampute.HttpClient` is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file for more details.
39 changes: 15 additions & 24 deletions src/Kampute.HttpClient.DataContract/HttpRestClientXmlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ namespace Kampute.HttpClient.DataContract
using System;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -52,7 +50,6 @@ public static void SetXmlSerializerSettings(this HttpRestClient client, DataCont
/// </summary>
/// <param name="client">The <see cref="HttpRestClient"/> instance to query.</param>
/// <returns>The <see cref="DataContractSerializerSettings"/> if set; otherwise, <c>null</c>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static DataContractSerializerSettings? GetXmlSerializerSettings(this HttpRestClient client)
{
serializerSettings.TryGetValue(client, out var settings);
Expand Down Expand Up @@ -95,8 +92,8 @@ public static XmlContentDeserializer AcceptXml(this HttpRestClient client, DataC
/// <exception cref="HttpResponseException">Thrown if the response status code indicates a failure.</exception>
/// <exception cref="HttpRequestException">Thrown if the request fails due to an underlying issue such as network connectivity, DNS failure, server certificate validation, or timeout.</exception>
/// <exception cref="HttpContentException">Thrown if the response body is empty or its media type is not supported.</exception>
/// <exception cref="TaskCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
public static async Task<T?> SendAsXmlAsync<T>
/// <exception cref="OperationCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
public static Task<T?> SendAsXmlAsync<T>
(
this HttpRestClient client,
HttpMethod method,
Expand All @@ -108,8 +105,8 @@ public static async Task<T?> SendAsXmlAsync<T>
if (payload is null)
throw new ArgumentNullException(nameof(payload));

using var content = new XmlContent(payload) { Settings = client.GetXmlSerializerSettings() };
return await client.SendAsync<T>(method, uri, content, cancellationToken).ConfigureAwait(false);
var xmlContent = new XmlContent(payload) { Settings = client.GetXmlSerializerSettings() };
return client.SendAsync<T>(method, uri, xmlContent, cancellationToken);
}

/// <summary>
Expand All @@ -120,13 +117,13 @@ public static async Task<T?> SendAsXmlAsync<T>
/// <param name="uri">The URI to which the request is sent.</param>
/// <param name="payload">The object to serialize as the XML-formatted HTTP request payload.</param>
/// <param name="cancellationToken">A token for canceling the request (optional).</param>
/// <returns>A task representing the asynchronous operation, returning headers of the response.</returns>
/// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="method"/>, <paramref name="uri"/> or <paramref name="payload"/> is <c>null</c>.</exception>
/// <exception cref="HttpResponseException">Thrown if the response status code indicates a failure.</exception>
/// <exception cref="HttpRequestException">Thrown if the request fails due to an underlying issue such as network connectivity, DNS failure, server certificate validation, or timeout.</exception>
/// <exception cref="HttpContentException">Thrown if the response body is empty or its media type is not supported.</exception>
/// <exception cref="TaskCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
public static async Task<HttpResponseHeaders> SendAsXmlAsync
/// <exception cref="OperationCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
public static async Task SendAsXmlAsync
(
this HttpRestClient client,
HttpMethod method,
Expand All @@ -138,8 +135,8 @@ public static async Task<HttpResponseHeaders> SendAsXmlAsync
if (payload is null)
throw new ArgumentNullException(nameof(payload));

using var content = new XmlContent(payload) { Settings = client.GetXmlSerializerSettings() };
return await client.SendAsync(method, uri, content, cancellationToken).ConfigureAwait(false);
var xmlContent = new XmlContent(payload) { Settings = client.GetXmlSerializerSettings() };
using var _ = await client.SendAsync(method, uri, xmlContent, cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand All @@ -155,8 +152,7 @@ public static async Task<HttpResponseHeaders> SendAsXmlAsync
/// <exception cref="HttpResponseException">Thrown if the response status code indicates a failure.</exception>
/// <exception cref="HttpRequestException">Thrown if the request fails due to an underlying issue such as network connectivity, DNS failure, server certificate validation, or timeout.</exception>
/// <exception cref="HttpContentException">Thrown if the response body is empty or its media type is not supported.</exception>
/// <exception cref="TaskCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <exception cref="OperationCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
public static Task<T?> PostAsXmlAsync<T>(this HttpRestClient client, string uri, object payload, CancellationToken cancellationToken = default)
{
return client.SendAsXmlAsync<T>(HttpVerb.Post, uri, payload, cancellationToken);
Expand All @@ -174,8 +170,7 @@ public static async Task<HttpResponseHeaders> SendAsXmlAsync
/// <exception cref="HttpResponseException">Thrown if the response status code indicates a failure.</exception>
/// <exception cref="HttpRequestException">Thrown if the request fails due to an underlying issue such as network connectivity, DNS failure, server certificate validation, or timeout.</exception>
/// <exception cref="HttpContentException">Thrown if the response body is empty or its media type is not supported.</exception>
/// <exception cref="TaskCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <exception cref="OperationCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
public static Task PostAsXmlAsync(this HttpRestClient client, string uri, object payload, CancellationToken cancellationToken = default)
{
return client.SendAsXmlAsync(HttpVerb.Post, uri, payload, cancellationToken);
Expand All @@ -194,8 +189,7 @@ public static Task PostAsXmlAsync(this HttpRestClient client, string uri, object
/// <exception cref="HttpResponseException">Thrown if the response status code indicates a failure.</exception>
/// <exception cref="HttpRequestException">Thrown if the request fails due to an underlying issue such as network connectivity, DNS failure, server certificate validation, or timeout.</exception>
/// <exception cref="HttpContentException">Thrown if the response body is empty or its media type is not supported.</exception>
/// <exception cref="TaskCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <exception cref="OperationCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
public static Task<T?> PutAsXmlAsync<T>(this HttpRestClient client, string uri, object payload, CancellationToken cancellationToken = default)
{
return client.SendAsXmlAsync<T>(HttpVerb.Put, uri, payload, cancellationToken);
Expand All @@ -213,8 +207,7 @@ public static Task PostAsXmlAsync(this HttpRestClient client, string uri, object
/// <exception cref="HttpResponseException">Thrown if the response status code indicates a failure.</exception>
/// <exception cref="HttpRequestException">Thrown if the request fails due to an underlying issue such as network connectivity, DNS failure, server certificate validation, or timeout.</exception>
/// <exception cref="HttpContentException">Thrown if the response body is empty or its media type is not supported.</exception>
/// <exception cref="TaskCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <exception cref="OperationCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
public static Task PutAsXmlAsync(this HttpRestClient client, string uri, object payload, CancellationToken cancellationToken = default)
{
return client.SendAsXmlAsync(HttpVerb.Put, uri, payload, cancellationToken);
Expand All @@ -233,8 +226,7 @@ public static Task PutAsXmlAsync(this HttpRestClient client, string uri, object
/// <exception cref="HttpResponseException">Thrown if the response status code indicates a failure.</exception>
/// <exception cref="HttpRequestException">Thrown if the request fails due to an underlying issue such as network connectivity, DNS failure, server certificate validation, or timeout.</exception>
/// <exception cref="HttpContentException">Thrown if the response body is empty or its media type is not supported.</exception>
/// <exception cref="TaskCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <exception cref="OperationCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
public static Task<T?> PatchAsXmlAsync<T>(this HttpRestClient client, string uri, object payload, CancellationToken cancellationToken = default)
{
return client.SendAsXmlAsync<T>(HttpVerb.Patch, uri, payload, cancellationToken);
Expand All @@ -252,8 +244,7 @@ public static Task PutAsXmlAsync(this HttpRestClient client, string uri, object
/// <exception cref="HttpResponseException">Thrown if the response status code indicates a failure.</exception>
/// <exception cref="HttpRequestException">Thrown if the request fails due to an underlying issue such as network connectivity, DNS failure, server certificate validation, or timeout.</exception>
/// <exception cref="HttpContentException">Thrown if the response body is empty or its media type is not supported.</exception>
/// <exception cref="TaskCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <exception cref="OperationCanceledException">Thrown if the operation is canceled via the cancellation token.</exception>
public static Task PatchAsXmlAsync(this HttpRestClient client, string uri, object payload, CancellationToken cancellationToken = default)
{
return client.SendAsXmlAsync(HttpVerb.Patch, uri, payload, cancellationToken);
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>1.6.0</Version>
<Version>2.0.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2024 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down
26 changes: 13 additions & 13 deletions src/Kampute.HttpClient.DataContract/XmlContentDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Kampute.HttpClient.DataContract
{
using Kampute.HttpClient.Interfaces;
using Kampute.HttpClient.Content.Abstracts;
using System;
using System.Collections.Generic;
using System.IO;
Expand All @@ -21,23 +21,23 @@ namespace Kampute.HttpClient.DataContract
/// <summary>
/// Provides functionality for deserializing XML content from HTTP responses into objects.
/// </summary>
public sealed class XmlContentDeserializer : IHttpContentDeserializer
public sealed class XmlContentDeserializer : HttpContentDeserializer
{
/// <summary>
/// Gets or sets the XML deserialization settings.
/// Initializes a new instance of the <see cref="XmlContentDeserializer"/> class.
/// </summary>
/// <value>
/// The XML deserialization settings, if any.
/// </value>
public DataContractSerializerSettings? Settings { get; set; }
public XmlContentDeserializer()
: base(MediaTypeNames.Application.Xml)
{
}

/// <summary>
/// Gets the collection of media types that this deserializer supports.
/// Gets or sets the XML deserialization settings.
/// </summary>
/// <value>
/// The read-only collection of media types that this deserializer supports.
/// The XML deserialization settings, if any.
/// </value>
public IReadOnlyCollection<string> SupportedMediaTypes { get; } = [MediaTypeNames.Application.Xml];
public DataContractSerializerSettings? Settings { get; set; }

/// <summary>
/// Retrieves a collection of supported media types for a specific model type.
Expand All @@ -47,7 +47,7 @@ public sealed class XmlContentDeserializer : IHttpContentDeserializer
/// The read-only collection of media types that this deserializer supports if the model type is not <c>null</c> and
/// is marked with a <see cref="DataContractAttribute"/>; otherwise, an empty collection.
/// </returns>
public IReadOnlyCollection<string> GetSupportedMediaTypes(Type? modelType)
public override IEnumerable<string> GetSupportedMediaTypes(Type modelType)
{
return modelType?.GetCustomAttribute<DataContractAttribute>() is not null ? SupportedMediaTypes : [];
}
Expand All @@ -61,7 +61,7 @@ public IReadOnlyCollection<string> GetSupportedMediaTypes(Type? modelType)
/// <c>true</c> if the deserializer supports the media type and the model type is not <c>null</c> and is marked with
/// a <see cref="DataContractAttribute"/>; otherwise, <c>false</c>.
/// </returns>
public bool CanDeserialize(string mediaType, Type? modelType)
public override bool CanDeserialize(string mediaType, Type modelType)
{
return modelType?.GetCustomAttribute<DataContractAttribute>() is not null && SupportedMediaTypes.Contains(mediaType);
}
Expand All @@ -74,7 +74,7 @@ public bool CanDeserialize(string mediaType, Type? modelType)
/// <param name="cancellationToken">A token for canceling the read operation (optional).</param>
/// <returns>A task representing the asynchronous read operation, containing the deserialized object.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="content"/> or <paramref name="modelType"/> is <c>null</c>.</exception>
public async Task<object?> DeserializeAsync(HttpContent content, Type modelType, CancellationToken cancellationToken = default)
public override async Task<object?> DeserializeAsync(HttpContent content, Type modelType, CancellationToken cancellationToken = default)
{
if (content is null)
throw new ArgumentNullException(nameof(content));
Expand Down
Loading

0 comments on commit 9cdd999

Please sign in to comment.