Skip to content

Commit

Permalink
Improve HttpContentDeserializerCollection and XML comments (#8)
Browse files Browse the repository at this point in the history
Co-authored-by: Kambiz Khojasteh <kambiz.khojasteh@gmail.com>
  • Loading branch information
kampute and Khojasteh authored May 6, 2024
1 parent 9cdd999 commit b552dc7
Show file tree
Hide file tree
Showing 21 changed files with 223 additions and 133 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ to address the complexities of web service consumption.
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.
Offers the capability to define request 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
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.0.0</Version>
<Version>2.1.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2024 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down
4 changes: 2 additions & 2 deletions 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.0.0</Version>
<Version>2.1.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2024 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down Expand Up @@ -36,7 +36,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" Version="[8.0.2,9.0.0)" />
<PackageReference Include="System.Text.Json" Version="8.0.3" />
</ItemGroup>

<ItemGroup>
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.0.0</Version>
<Version>2.1.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2024 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down Expand Up @@ -36,7 +36,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="[13.0.3,14.0.0)" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

<ItemGroup>
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.0.0</Version>
<Version>2.1.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2024 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down
218 changes: 143 additions & 75 deletions src/Kampute.HttpClient/HttpContentDeserializerCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,39 @@
namespace Kampute.HttpClient
{
using Kampute.HttpClient.Interfaces;
using Kampute.HttpClient.Utilities;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;

/// <summary>
/// Represents a specialized collection of <see cref="IHttpContentDeserializer"/> instances.
/// </summary>
/// <remarks>
/// <para>
/// This collection provides capabilities for managing <see cref="IHttpContentDeserializer"/> instances, including adding, removing,
/// and selecting deserializers based on media types and model types. It leverages internal caches to optimize performance for frequently
/// accessed deserializers, significantly enhancing efficiency in scenarios where media types and model types are repeatedly queried.
/// </para>
/// <para>
/// Caches within the collection are automatically invalidated and updated upon modification of the deserializer inventory, ensuring that access
/// patterns remain efficient and that overhead associated with dynamic updates is minimized.
/// </para>
/// <para>
/// The collection is designed to be flexible and adaptable, accommodating a wide range of models, media types, and server behaviors without
/// prior knowledge of specific implementations. It supports assigning quality factors to media types, prioritizing those that can deserialize
/// both model and error types (q=1.0) over those that solely support error types (q=0.9). This nuanced handling of media types facilitates
/// sophisticated content negotiation strategies, ensuring clients can effectively communicate preferences for both successful responses and
/// error scenarios.
/// </para>
/// </remarks>
public sealed class HttpContentDeserializerCollection : ICollection<IHttpContentDeserializer>
public sealed class HttpContentDeserializerCollection : ICollection<IHttpContentDeserializer>, IReadOnlyCollection<IHttpContentDeserializer>
{
private static readonly string[] AllMediaTypes = ["*/*"];

private readonly List<IHttpContentDeserializer> _collection = [];
private readonly ConcurrentDictionary<Type, IReadOnlyCollection<string>> _mediaTypes1Cache = new();
private readonly ConcurrentDictionary<(Type, Type), IReadOnlyCollection<string>> _mediaTypes2Cache = new();
private readonly List<IHttpContentDeserializer> _collection;
private readonly Lazy<AcceptableMediaTypeCache> _acceptCache;
private readonly FlyweightCache<(string, Type), IHttpContentDeserializer?> _deserializerCache;

/// <summary>
/// Initializes a new instance of the <see cref="HttpContentDeserializerCollection"/> class.
/// </summary>
public HttpContentDeserializerCollection()
{
_collection = [];
_deserializerCache = new(key => FindDeserializer(key.Item1, key.Item2));
_acceptCache = new(() => new(this), LazyThreadSafetyMode.PublicationOnly);
}

/// <summary>
/// Gets the number of <see cref="IHttpContentDeserializer"/> instances contained in the collection.
Expand All @@ -66,11 +64,7 @@ public sealed class HttpContentDeserializerCollection : ICollection<IHttpContent
/// <returns>An instance of <see cref="IHttpContentDeserializer"/> that can deserialize the specified media type and model type, or <c>null</c> if none is found.</returns>
public IHttpContentDeserializer? GetDeserializerFor(string mediaType, Type modelType)
{
foreach (var deserializer in _collection)
if (deserializer.CanDeserialize(mediaType, modelType))
return deserializer;

return null;
return _deserializerCache.Get((mediaType, modelType));
}

/// <summary>
Expand Down Expand Up @@ -98,7 +92,7 @@ public IEnumerable<string> GetAcceptableMediaTypes(Type? modelType)
{
0 => [],
1 => _collection[0].GetSupportedMediaTypes(modelType),
_ => _mediaTypes1Cache.GetOrAdd(modelType, CollectSupportedMediaTypes)
_ => _acceptCache.Value.GetSupportedMediaTypes(modelType)
};
}

Expand Down Expand Up @@ -135,7 +129,7 @@ public IEnumerable<string> GetAcceptableMediaTypes(Type? modelType, Type? errorT
if (modelType is null)
return GetAcceptableMediaTypes(errorType).Concat(AllMediaTypes);

return _mediaTypes2Cache.GetOrAdd((modelType, errorType), CollectSupportedMediaTypes);
return _acceptCache.Value.GetSupportedMediaTypes(modelType, errorType);
}

/// <summary>
Expand Down Expand Up @@ -230,75 +224,149 @@ IEnumerator IEnumerable.GetEnumerator()
}

/// <summary>
/// Resets the caches.
/// Locates the first <see cref="IHttpContentDeserializer"/> instances in the collection that support deserializing a specific media type and model type.
/// </summary>
private void InvalidateCaches()
/// <param name="mediaType">The media type to deserialize.</param>
/// <param name="modelType">The type of the model to deserialize.</param>
/// <returns>An instance of <see cref="IHttpContentDeserializer"/> that can deserialize the specified media type and model type, or <c>null</c> if none is found.</returns>
private IHttpContentDeserializer? FindDeserializer(string mediaType, Type modelType)
{
_mediaTypes1Cache.Clear();
_mediaTypes2Cache.Clear();
foreach (var deserializer in _collection)
if (deserializer.CanDeserialize(mediaType, modelType))
return deserializer;

return null;
}

/// <summary>
/// Retrieves all supported media types for a specified model type from the collection of deserializers.
/// Resets the caches.
/// </summary>
/// <param name="modelType">The type of the model for which to retrieve supported media type header values.</param>
/// <returns>A read-only collection of strings that represent the media types supported for deserializing the specified model type.</returns>
private IReadOnlyCollection<string> CollectSupportedMediaTypes(Type modelType)
private void InvalidateCaches()
{
var uniqueMediaTypes = new HashSet<string>();
var orderedMediaTypes = new List<string>();
foreach (var deserializer in _collection)
{
foreach (var mediaType in deserializer.GetSupportedMediaTypes(modelType))
{
if (uniqueMediaTypes.Add(mediaType))
orderedMediaTypes.Add(mediaType);
}
}
orderedMediaTypes.TrimExcess();
return orderedMediaTypes;
_deserializerCache.Clear();
if (_acceptCache.IsValueCreated)
_acceptCache.Value.Clear();
}

#region Helper Types

/// <summary>
/// Retrieves all supported media types for a specified pair of model and error types from the collection of deserializers.
/// Provides cache of supported media types for .NET object types.
/// </summary>
/// <param name="types">
/// A tuple containing two types used to collect and aggregate media types that can deserialize objects of these types from HTTP content:
/// <list type="bullet">
/// <item>
/// <term>Item1</term>
/// <description>The type of the model for which to retrieve supported media types.</description>
/// </item>
/// <item>
/// <term>Item2</term>
/// <description>The type of the error for which to retrieve supported media types.</description>
/// </item>
/// </list>
/// </param>
/// <returns>
/// A read-only collection of strings that represent the media types supported for deserializing the specified types.
/// </returns>
private IReadOnlyCollection<string> CollectSupportedMediaTypes((Type, Type) types)
private sealed class AcceptableMediaTypeCache
{
var uniqueMediaTypes = new HashSet<string>();
var orderedMediaTypes = new List<string>();
private readonly IReadOnlyCollection<IHttpContentDeserializer> _deserializers;
private readonly FlyweightCache<Type, IReadOnlyCollection<string>> _singles;
private readonly FlyweightCache<(Type, Type), IReadOnlyCollection<string>> _duals;

// Add media type header values supporting model
foreach (var mediaType in GetAcceptableMediaTypes(types.Item1))
public AcceptableMediaTypeCache(IReadOnlyCollection<IHttpContentDeserializer> deserializers)
{
if (uniqueMediaTypes.Add(mediaType))
orderedMediaTypes.Add(mediaType);
_deserializers = deserializers;
_singles = new(CollectSupportedMediaTypes);
_duals = new(CollectSupportedMediaTypes);
}

// Add media type header values supporting error
foreach (var mediaType in GetAcceptableMediaTypes(types.Item2))
/// <summary>
/// Retrieves all supported media types for a specified model type from the collection of deserializers.
/// </summary>
/// <param name="modelType">The type of the model for which to retrieve supported media types.</param>
/// <returns>A read-only collection of strings that represent the media types supported for deserializing the specified model type.</returns>
public IReadOnlyCollection<string> GetSupportedMediaTypes(Type modelType)
{
if (uniqueMediaTypes.Add(mediaType))
orderedMediaTypes.Add(mediaType);
return _singles.Get(modelType);
}

/// <summary>
/// Retrieves all supported media types for a specified model type and error type from the collection of deserializers.
/// </summary>
/// <param name="modelType">The type of the model for which to retrieve supported media types.</param>
/// <param name="errorType">The type of the error for which to retrieve supported media types.</param>
/// <returns>A read-only collection of strings representing the supported media types for the specified types.</returns>
public IReadOnlyCollection<string> GetSupportedMediaTypes(Type modelType, Type errorType)
{
return _duals.Get((modelType, errorType));
}

/// <summary>
/// Clears the chace.
/// </summary>
public void Clear()
{
_singles.Clear();
_duals.Clear();
}

/// <summary>
/// Retrieves all supported media types for a specified model type from the collection of deserializers.
/// </summary>
/// <param name="modelType">The type of the model for which to retrieve supported media type header values.</param>
/// <returns>A read-only collection of strings that represent the media types supported for deserializing the specified model type.</returns>
private IReadOnlyCollection<string> CollectSupportedMediaTypes(Type modelType)
{
var uniqueMediaTypes = new HashSet<string>();
var orderedMediaTypes = new List<string>();

foreach (var deserializer in _deserializers)
{
foreach (var mediaType in deserializer.GetSupportedMediaTypes(modelType))
{
if (uniqueMediaTypes.Add(mediaType))
orderedMediaTypes.Add(mediaType);
}
}

orderedMediaTypes.TrimExcess();
return orderedMediaTypes;
}

orderedMediaTypes.TrimExcess();
return orderedMediaTypes;
/// <summary>
/// Retrieves all supported media types for a specified pair of model and error types from the collection of deserializers.
/// </summary>
/// <param name="types">
/// A tuple containing two types used to collect and aggregate media types that can deserialize objects of these types from HTTP content:
/// <list type="bullet">
/// <item>
/// <term>Item1</term>
/// <description>The type of the model for which to retrieve supported media types.</description>
/// </item>
/// <item>
/// <term>Item2</term>
/// <description>The type of the error for which to retrieve supported media types.</description>
/// </item>
/// </list>
/// </param>
/// <returns>
/// A read-only collection of strings that represent the media types supported for deserializing the specified types.
/// </returns>
private IReadOnlyCollection<string> CollectSupportedMediaTypes((Type, Type) types)
{
var (modelType, errorType) = types;
var uniqueMediaTypes = new HashSet<string>();
var orderedMediaTypes = new List<string>();

foreach (var deserializer in _deserializers)
{
foreach (var mediaType in deserializer.GetSupportedMediaTypes(modelType))
{
if (uniqueMediaTypes.Add(mediaType))
orderedMediaTypes.Add(mediaType);
}
}

foreach (var deserializer in _deserializers)
{
foreach (var mediaType in deserializer.GetSupportedMediaTypes(errorType))
{
if (uniqueMediaTypes.Add(mediaType))
orderedMediaTypes.Add(mediaType);
}
}

orderedMediaTypes.TrimExcess();
return orderedMediaTypes;
}
}

#endregion
}
}
2 changes: 1 addition & 1 deletion src/Kampute.HttpClient/HttpContentException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Kampute.HttpClient
/// <summary>
/// The exception that is thrown when an invalid or unsupported content is encountered in an HTTP response.
/// </summary>
public class HttpContentException : ApplicationException
public class HttpContentException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpContentException"/> class.
Expand Down
Loading

0 comments on commit b552dc7

Please sign in to comment.