Skip to content

Commit b552dc7

Browse files
kamputeKhojasteh
andauthored
Improve HttpContentDeserializerCollection and XML comments (#8)
Co-authored-by: Kambiz Khojasteh <kambiz.khojasteh@gmail.com>
1 parent 9cdd999 commit b552dc7

21 files changed

Lines changed: 223 additions & 133 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ to address the complexities of web service consumption.
2020
mechanisms to fit specific application needs.
2121

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

2727
- **Custom Error Handling and Exception Management:**
2828
Converts HTTP response errors into detailed, meaningful exceptions, streamlining the process of interpreting API-specific errors with the aid of a customizable

src/Kampute.HttpClient.DataContract/Kampute.HttpClient.DataContract.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<Title>Kampute.HttpClient.DataContract</Title>
66
<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>
77
<Authors>Kambiz Khojasteh</Authors>
8-
<Version>2.0.0</Version>
8+
<Version>2.1.0</Version>
99
<Company>Kampute</Company>
1010
<Copyright>Copyright (c) 2024 Kampute</Copyright>
1111
<LangVersion>latest</LangVersion>

src/Kampute.HttpClient.Json/Kampute.HttpClient.Json.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<Title>Kampute.HttpClient.Json</Title>
66
<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>
77
<Authors>Kambiz Khojasteh</Authors>
8-
<Version>2.0.0</Version>
8+
<Version>2.1.0</Version>
99
<Company>Kampute</Company>
1010
<Copyright>Copyright (c) 2024 Kampute</Copyright>
1111
<LangVersion>latest</LangVersion>
@@ -36,7 +36,7 @@
3636
</ItemGroup>
3737

3838
<ItemGroup>
39-
<PackageReference Include="System.Text.Json" Version="[8.0.2,9.0.0)" />
39+
<PackageReference Include="System.Text.Json" Version="8.0.3" />
4040
</ItemGroup>
4141

4242
<ItemGroup>

src/Kampute.HttpClient.NewtonsoftJson/Kampute.HttpClient.NewtonsoftJson.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<Title>Kampute.HttpClient.NewtonsoftJson</Title>
66
<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>
77
<Authors>Kambiz Khojasteh</Authors>
8-
<Version>2.0.0</Version>
8+
<Version>2.1.0</Version>
99
<Company>Kampute</Company>
1010
<Copyright>Copyright (c) 2024 Kampute</Copyright>
1111
<LangVersion>latest</LangVersion>
@@ -36,7 +36,7 @@
3636
</ItemGroup>
3737

3838
<ItemGroup>
39-
<PackageReference Include="Newtonsoft.Json" Version="[13.0.3,14.0.0)" />
39+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
4040
</ItemGroup>
4141

4242
<ItemGroup>

src/Kampute.HttpClient.Xml/Kampute.HttpClient.Xml.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<Title>Kampute.HttpClient.Xml</Title>
66
<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>
77
<Authors>Kambiz Khojasteh</Authors>
8-
<Version>2.0.0</Version>
8+
<Version>2.1.0</Version>
99
<Company>Kampute</Company>
1010
<Copyright>Copyright (c) 2024 Kampute</Copyright>
1111
<LangVersion>latest</LangVersion>

src/Kampute.HttpClient/HttpContentDeserializerCollection.cs

Lines changed: 143 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,39 @@
66
namespace Kampute.HttpClient
77
{
88
using Kampute.HttpClient.Interfaces;
9+
using Kampute.HttpClient.Utilities;
910
using System;
1011
using System.Collections;
11-
using System.Collections.Concurrent;
1212
using System.Collections.Generic;
1313
using System.Linq;
1414
using System.Runtime.CompilerServices;
15+
using System.Threading;
1516

1617
/// <summary>
1718
/// Represents a specialized collection of <see cref="IHttpContentDeserializer"/> instances.
1819
/// </summary>
1920
/// <remarks>
20-
/// <para>
2121
/// This collection provides capabilities for managing <see cref="IHttpContentDeserializer"/> instances, including adding, removing,
2222
/// and selecting deserializers based on media types and model types. It leverages internal caches to optimize performance for frequently
2323
/// accessed deserializers, significantly enhancing efficiency in scenarios where media types and model types are repeatedly queried.
24-
/// </para>
25-
/// <para>
26-
/// Caches within the collection are automatically invalidated and updated upon modification of the deserializer inventory, ensuring that access
27-
/// patterns remain efficient and that overhead associated with dynamic updates is minimized.
28-
/// </para>
29-
/// <para>
30-
/// The collection is designed to be flexible and adaptable, accommodating a wide range of models, media types, and server behaviors without
31-
/// prior knowledge of specific implementations. It supports assigning quality factors to media types, prioritizing those that can deserialize
32-
/// 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
33-
/// sophisticated content negotiation strategies, ensuring clients can effectively communicate preferences for both successful responses and
34-
/// error scenarios.
35-
/// </para>
3624
/// </remarks>
37-
public sealed class HttpContentDeserializerCollection : ICollection<IHttpContentDeserializer>
25+
public sealed class HttpContentDeserializerCollection : ICollection<IHttpContentDeserializer>, IReadOnlyCollection<IHttpContentDeserializer>
3826
{
3927
private static readonly string[] AllMediaTypes = ["*/*"];
4028

41-
private readonly List<IHttpContentDeserializer> _collection = [];
42-
private readonly ConcurrentDictionary<Type, IReadOnlyCollection<string>> _mediaTypes1Cache = new();
43-
private readonly ConcurrentDictionary<(Type, Type), IReadOnlyCollection<string>> _mediaTypes2Cache = new();
29+
private readonly List<IHttpContentDeserializer> _collection;
30+
private readonly Lazy<AcceptableMediaTypeCache> _acceptCache;
31+
private readonly FlyweightCache<(string, Type), IHttpContentDeserializer?> _deserializerCache;
32+
33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="HttpContentDeserializerCollection"/> class.
35+
/// </summary>
36+
public HttpContentDeserializerCollection()
37+
{
38+
_collection = [];
39+
_deserializerCache = new(key => FindDeserializer(key.Item1, key.Item2));
40+
_acceptCache = new(() => new(this), LazyThreadSafetyMode.PublicationOnly);
41+
}
4442

4543
/// <summary>
4644
/// Gets the number of <see cref="IHttpContentDeserializer"/> instances contained in the collection.
@@ -66,11 +64,7 @@ public sealed class HttpContentDeserializerCollection : ICollection<IHttpContent
6664
/// <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>
6765
public IHttpContentDeserializer? GetDeserializerFor(string mediaType, Type modelType)
6866
{
69-
foreach (var deserializer in _collection)
70-
if (deserializer.CanDeserialize(mediaType, modelType))
71-
return deserializer;
72-
73-
return null;
67+
return _deserializerCache.Get((mediaType, modelType));
7468
}
7569

7670
/// <summary>
@@ -98,7 +92,7 @@ public IEnumerable<string> GetAcceptableMediaTypes(Type? modelType)
9892
{
9993
0 => [],
10094
1 => _collection[0].GetSupportedMediaTypes(modelType),
101-
_ => _mediaTypes1Cache.GetOrAdd(modelType, CollectSupportedMediaTypes)
95+
_ => _acceptCache.Value.GetSupportedMediaTypes(modelType)
10296
};
10397
}
10498

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

138-
return _mediaTypes2Cache.GetOrAdd((modelType, errorType), CollectSupportedMediaTypes);
132+
return _acceptCache.Value.GetSupportedMediaTypes(modelType, errorType);
139133
}
140134

141135
/// <summary>
@@ -230,75 +224,149 @@ IEnumerator IEnumerable.GetEnumerator()
230224
}
231225

232226
/// <summary>
233-
/// Resets the caches.
227+
/// Locates the first <see cref="IHttpContentDeserializer"/> instances in the collection that support deserializing a specific media type and model type.
234228
/// </summary>
235-
private void InvalidateCaches()
229+
/// <param name="mediaType">The media type to deserialize.</param>
230+
/// <param name="modelType">The type of the model to deserialize.</param>
231+
/// <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>
232+
private IHttpContentDeserializer? FindDeserializer(string mediaType, Type modelType)
236233
{
237-
_mediaTypes1Cache.Clear();
238-
_mediaTypes2Cache.Clear();
234+
foreach (var deserializer in _collection)
235+
if (deserializer.CanDeserialize(mediaType, modelType))
236+
return deserializer;
237+
238+
return null;
239239
}
240240

241241
/// <summary>
242-
/// Retrieves all supported media types for a specified model type from the collection of deserializers.
242+
/// Resets the caches.
243243
/// </summary>
244-
/// <param name="modelType">The type of the model for which to retrieve supported media type header values.</param>
245-
/// <returns>A read-only collection of strings that represent the media types supported for deserializing the specified model type.</returns>
246-
private IReadOnlyCollection<string> CollectSupportedMediaTypes(Type modelType)
244+
private void InvalidateCaches()
247245
{
248-
var uniqueMediaTypes = new HashSet<string>();
249-
var orderedMediaTypes = new List<string>();
250-
foreach (var deserializer in _collection)
251-
{
252-
foreach (var mediaType in deserializer.GetSupportedMediaTypes(modelType))
253-
{
254-
if (uniqueMediaTypes.Add(mediaType))
255-
orderedMediaTypes.Add(mediaType);
256-
}
257-
}
258-
orderedMediaTypes.TrimExcess();
259-
return orderedMediaTypes;
246+
_deserializerCache.Clear();
247+
if (_acceptCache.IsValueCreated)
248+
_acceptCache.Value.Clear();
260249
}
261250

251+
#region Helper Types
252+
262253
/// <summary>
263-
/// Retrieves all supported media types for a specified pair of model and error types from the collection of deserializers.
254+
/// Provides cache of supported media types for .NET object types.
264255
/// </summary>
265-
/// <param name="types">
266-
/// A tuple containing two types used to collect and aggregate media types that can deserialize objects of these types from HTTP content:
267-
/// <list type="bullet">
268-
/// <item>
269-
/// <term>Item1</term>
270-
/// <description>The type of the model for which to retrieve supported media types.</description>
271-
/// </item>
272-
/// <item>
273-
/// <term>Item2</term>
274-
/// <description>The type of the error for which to retrieve supported media types.</description>
275-
/// </item>
276-
/// </list>
277-
/// </param>
278-
/// <returns>
279-
/// A read-only collection of strings that represent the media types supported for deserializing the specified types.
280-
/// </returns>
281-
private IReadOnlyCollection<string> CollectSupportedMediaTypes((Type, Type) types)
256+
private sealed class AcceptableMediaTypeCache
282257
{
283-
var uniqueMediaTypes = new HashSet<string>();
284-
var orderedMediaTypes = new List<string>();
258+
private readonly IReadOnlyCollection<IHttpContentDeserializer> _deserializers;
259+
private readonly FlyweightCache<Type, IReadOnlyCollection<string>> _singles;
260+
private readonly FlyweightCache<(Type, Type), IReadOnlyCollection<string>> _duals;
285261

286-
// Add media type header values supporting model
287-
foreach (var mediaType in GetAcceptableMediaTypes(types.Item1))
262+
public AcceptableMediaTypeCache(IReadOnlyCollection<IHttpContentDeserializer> deserializers)
288263
{
289-
if (uniqueMediaTypes.Add(mediaType))
290-
orderedMediaTypes.Add(mediaType);
264+
_deserializers = deserializers;
265+
_singles = new(CollectSupportedMediaTypes);
266+
_duals = new(CollectSupportedMediaTypes);
291267
}
292268

293-
// Add media type header values supporting error
294-
foreach (var mediaType in GetAcceptableMediaTypes(types.Item2))
269+
/// <summary>
270+
/// Retrieves all supported media types for a specified model type from the collection of deserializers.
271+
/// </summary>
272+
/// <param name="modelType">The type of the model for which to retrieve supported media types.</param>
273+
/// <returns>A read-only collection of strings that represent the media types supported for deserializing the specified model type.</returns>
274+
public IReadOnlyCollection<string> GetSupportedMediaTypes(Type modelType)
295275
{
296-
if (uniqueMediaTypes.Add(mediaType))
297-
orderedMediaTypes.Add(mediaType);
276+
return _singles.Get(modelType);
277+
}
278+
279+
/// <summary>
280+
/// Retrieves all supported media types for a specified model type and error type from the collection of deserializers.
281+
/// </summary>
282+
/// <param name="modelType">The type of the model for which to retrieve supported media types.</param>
283+
/// <param name="errorType">The type of the error for which to retrieve supported media types.</param>
284+
/// <returns>A read-only collection of strings representing the supported media types for the specified types.</returns>
285+
public IReadOnlyCollection<string> GetSupportedMediaTypes(Type modelType, Type errorType)
286+
{
287+
return _duals.Get((modelType, errorType));
288+
}
289+
290+
/// <summary>
291+
/// Clears the chace.
292+
/// </summary>
293+
public void Clear()
294+
{
295+
_singles.Clear();
296+
_duals.Clear();
297+
}
298+
299+
/// <summary>
300+
/// Retrieves all supported media types for a specified model type from the collection of deserializers.
301+
/// </summary>
302+
/// <param name="modelType">The type of the model for which to retrieve supported media type header values.</param>
303+
/// <returns>A read-only collection of strings that represent the media types supported for deserializing the specified model type.</returns>
304+
private IReadOnlyCollection<string> CollectSupportedMediaTypes(Type modelType)
305+
{
306+
var uniqueMediaTypes = new HashSet<string>();
307+
var orderedMediaTypes = new List<string>();
308+
309+
foreach (var deserializer in _deserializers)
310+
{
311+
foreach (var mediaType in deserializer.GetSupportedMediaTypes(modelType))
312+
{
313+
if (uniqueMediaTypes.Add(mediaType))
314+
orderedMediaTypes.Add(mediaType);
315+
}
316+
}
317+
318+
orderedMediaTypes.TrimExcess();
319+
return orderedMediaTypes;
298320
}
299321

300-
orderedMediaTypes.TrimExcess();
301-
return orderedMediaTypes;
322+
/// <summary>
323+
/// Retrieves all supported media types for a specified pair of model and error types from the collection of deserializers.
324+
/// </summary>
325+
/// <param name="types">
326+
/// A tuple containing two types used to collect and aggregate media types that can deserialize objects of these types from HTTP content:
327+
/// <list type="bullet">
328+
/// <item>
329+
/// <term>Item1</term>
330+
/// <description>The type of the model for which to retrieve supported media types.</description>
331+
/// </item>
332+
/// <item>
333+
/// <term>Item2</term>
334+
/// <description>The type of the error for which to retrieve supported media types.</description>
335+
/// </item>
336+
/// </list>
337+
/// </param>
338+
/// <returns>
339+
/// A read-only collection of strings that represent the media types supported for deserializing the specified types.
340+
/// </returns>
341+
private IReadOnlyCollection<string> CollectSupportedMediaTypes((Type, Type) types)
342+
{
343+
var (modelType, errorType) = types;
344+
var uniqueMediaTypes = new HashSet<string>();
345+
var orderedMediaTypes = new List<string>();
346+
347+
foreach (var deserializer in _deserializers)
348+
{
349+
foreach (var mediaType in deserializer.GetSupportedMediaTypes(modelType))
350+
{
351+
if (uniqueMediaTypes.Add(mediaType))
352+
orderedMediaTypes.Add(mediaType);
353+
}
354+
}
355+
356+
foreach (var deserializer in _deserializers)
357+
{
358+
foreach (var mediaType in deserializer.GetSupportedMediaTypes(errorType))
359+
{
360+
if (uniqueMediaTypes.Add(mediaType))
361+
orderedMediaTypes.Add(mediaType);
362+
}
363+
}
364+
365+
orderedMediaTypes.TrimExcess();
366+
return orderedMediaTypes;
367+
}
302368
}
369+
370+
#endregion
303371
}
304372
}

src/Kampute.HttpClient/HttpContentException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Kampute.HttpClient
1212
/// <summary>
1313
/// The exception that is thrown when an invalid or unsupported content is encountered in an HTTP response.
1414
/// </summary>
15-
public class HttpContentException : ApplicationException
15+
public class HttpContentException : Exception
1616
{
1717
/// <summary>
1818
/// Initializes a new instance of the <see cref="HttpContentException"/> class.

0 commit comments

Comments
 (0)