Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/content-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The base package registers no content formatter. Each format registers its forma

## JSON

Install either JSON package as described in [Getting started](getting-started.md). Pass serializer options when registering the formatter:
Both JSON formatters read `application/json`, `application/problem+json`, and any other media type with the `+json` suffix, such as `application/vnd.example+json`, and write `application/json`. Install either JSON package as described in [Getting started](getting-started.md). Pass serializer options when registering the formatter:

```csharp
using System.Text.Json;
Expand All @@ -28,7 +28,7 @@ For [`Newtonsoft.Json`](https://www.newtonsoft.com/json/help/html/N_Newtonsoft_J

## XML

[`UseXml()`](~/api/Kampute.HttpClient.Xml.HttpRestClientXmlExtensions.html#Kampute_HttpClient_Xml_HttpRestClientXmlExtensions_UseXml_Kampute_HttpClient_HttpRestClient_System_Action{Kampute_HttpClient_Xml_XmlFormatter}_) registers an [`XmlFormatter`](~/api/Kampute.HttpClient.Xml.XmlFormatter.html). Its [`Serializer`](~/api/Kampute.HttpClient.Xml.XmlFormatter.html#Kampute_HttpClient_Xml_XmlFormatter_Serializer) setting chooses the serializer. With the default, [`XmlSerializerKind.Auto`](~/api/Kampute.HttpClient.Xml.XmlSerializerKind.html#fields), types marked with [`[DataContract]`](https://learn.microsoft.com/dotnet/api/system.runtime.serialization.datacontractattribute) or [`[CollectionDataContract]`](https://learn.microsoft.com/dotnet/api/system.runtime.serialization.collectiondatacontractattribute) use [`DataContractSerializer`](https://learn.microsoft.com/dotnet/api/system.runtime.serialization.datacontractserializer), and all other types use [`XmlSerializer`](https://learn.microsoft.com/dotnet/api/system.xml.serialization.xmlserializer). The rule applies to responses by the requested type and to payloads by their runtime type. Set [`XmlSerializerKind.XmlSerializer`](~/api/Kampute.HttpClient.Xml.XmlSerializerKind.html#fields) or [`XmlSerializerKind.DataContractSerializer`](~/api/Kampute.HttpClient.Xml.XmlSerializerKind.html#fields) to use one serializer for every type, and [`DataContractSettings`](~/api/Kampute.HttpClient.Xml.XmlFormatter.html#Kampute_HttpClient_Xml_XmlFormatter_DataContractSettings) to configure [`DataContractSerializer`](https://learn.microsoft.com/dotnet/api/system.runtime.serialization.datacontractserializer).
[`UseXml()`](~/api/Kampute.HttpClient.Xml.HttpRestClientXmlExtensions.html#Kampute_HttpClient_Xml_HttpRestClientXmlExtensions_UseXml_Kampute_HttpClient_HttpRestClient_System_Action{Kampute_HttpClient_Xml_XmlFormatter}_) registers an [`XmlFormatter`](~/api/Kampute.HttpClient.Xml.XmlFormatter.html), which reads `application/xml`, `text/xml`, `application/problem+xml`, and any other media type with the `+xml` suffix, and writes `application/xml`. Its [`Serializer`](~/api/Kampute.HttpClient.Xml.XmlFormatter.html#Kampute_HttpClient_Xml_XmlFormatter_Serializer) setting chooses the serializer. With the default, [`XmlSerializerKind.Auto`](~/api/Kampute.HttpClient.Xml.XmlSerializerKind.html#fields), types marked with [`[DataContract]`](https://learn.microsoft.com/dotnet/api/system.runtime.serialization.datacontractattribute) or [`[CollectionDataContract]`](https://learn.microsoft.com/dotnet/api/system.runtime.serialization.collectiondatacontractattribute) use [`DataContractSerializer`](https://learn.microsoft.com/dotnet/api/system.runtime.serialization.datacontractserializer), and all other types use [`XmlSerializer`](https://learn.microsoft.com/dotnet/api/system.xml.serialization.xmlserializer). The rule applies to responses by the requested type and to payloads by their runtime type. Set [`XmlSerializerKind.XmlSerializer`](~/api/Kampute.HttpClient.Xml.XmlSerializerKind.html#fields) or [`XmlSerializerKind.DataContractSerializer`](~/api/Kampute.HttpClient.Xml.XmlSerializerKind.html#fields) to use one serializer for every type, and [`DataContractSettings`](~/api/Kampute.HttpClient.Xml.XmlFormatter.html#Kampute_HttpClient_Xml_XmlFormatter_DataContractSettings) to configure [`DataContractSerializer`](https://learn.microsoft.com/dotnet/api/system.runtime.serialization.datacontractserializer).

The following POST writes to your API; `Resource` is your application's serializable model.

Expand All @@ -45,7 +45,9 @@ await client.PostAsXmlAsync("https://api.example.com/resources", new Resource {

## Custom Formatters

You can also implement a content formatter for an application-specific content type. Derive from [`HttpContentFormatter`](~/api/Kampute.HttpClient.Content.Abstracts.HttpContentFormatter.html) and pass the media types it reads and the media types it writes to the base constructor. Override [`ReadContentAsync`](~/api/Kampute.HttpClient.Content.Abstracts.HttpContentFormatter.html#Kampute_HttpClient_Content_Abstracts_HttpContentFormatter_ReadContentAsync_System_Net_Http_HttpContent_System_Type_System_Threading_CancellationToken_) to read responses, [`CreateContent`](~/api/Kampute.HttpClient.Content.Abstracts.HttpContentFormatter.html#Kampute_HttpClient_Content_Abstracts_HttpContentFormatter_CreateContent_System_Object_System_String_) to write request payloads, or both. A formatter that only reads passes an empty list of writable media types, and one that only writes passes an empty list of readable media types.
A response with a `+json` or `+xml` media type, such as `application/vnd.example.resource+json`, does not need its own formatter, because the JSON and XML formatters read it. Implement a custom formatter when you need to write such a media type, advertise it in the `Accept` header, or read it differently. The client reads a response with the first registered formatter that can read it, so add a custom formatter for a `+json` or `+xml` media type before the JSON or XML formatter.

You can also implement a content formatter for an application-specific content type. Derive from [`HttpContentFormatter`](~/api/Kampute.HttpClient.Content.Abstracts.HttpContentFormatter.html) and pass the media types it reads and the media types it writes to the base constructor. Override [`ReadContentAsync`](~/api/Kampute.HttpClient.Content.Abstracts.HttpContentFormatter.html#Kampute_HttpClient_Content_Abstracts_HttpContentFormatter_ReadContentAsync_System_Net_Http_HttpContent_System_Type_System_Threading_CancellationToken_) to read responses, [`CreateContent`](~/api/Kampute.HttpClient.Content.Abstracts.HttpContentFormatter.html#Kampute_HttpClient_Content_Abstracts_HttpContentFormatter_CreateContent_System_Object_System_String_) to write request payloads, or both. A formatter that only reads passes an empty list of writable media types, and one that only writes passes an empty list of readable media types. To read media types beyond the listed ones, override [`CanReadMediaType`](~/api/Kampute.HttpClient.Content.Abstracts.HttpContentFormatter.html#Kampute_HttpClient_Content_Abstracts_HttpContentFormatter_CanReadMediaType_System_String_); [`HasStructuredSyntaxSuffix`](~/api/Kampute.HttpClient.Content.Abstracts.HttpContentFormatter.html#Kampute_HttpClient_Content_Abstracts_HttpContentFormatter_HasStructuredSyntaxSuffix_System_String_System_String_) tells whether a media type ends with a suffix such as `+json`. Media types accepted this way are read but not advertised in the `Accept` header.

This skeleton shows the overrides to implement; replace both [`NotImplementedException`](https://learn.microsoft.com/dotnet/api/system.notimplementedexception) statements with your format's read and write logic before registering it.

Expand Down
2 changes: 1 addition & 1 deletion docs/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The retry strategy comes from the [`Kampute.Resilience`](https://kampute.github.

## Structured Error Bodies

Set [`ResponseErrorType`](~/api/Kampute.HttpClient.HttpRestClient.html#Kampute_HttpClient_HttpRestClient_ResponseErrorType) to your API's error model. Register a [content formatter](content-formats.md) that can read that model from the response media type.
Set [`ResponseErrorType`](~/api/Kampute.HttpClient.HttpRestClient.html#Kampute_HttpClient_HttpRestClient_ResponseErrorType) to your API's error model. Register a [content formatter](content-formats.md) that can read that model from the response media type. The JSON and XML formatters also read RFC 9457 problem details, `application/problem+json` and `application/problem+xml`, so an error model with the problem details fields works with them.

The deserialized model is available through [`HttpResponseException.ResponseObject`](~/api/Kampute.HttpClient.HttpResponseException.html#Kampute_HttpClient_HttpResponseException_ResponseObject). If the model implements [`IHttpErrorResponse`](~/api/Kampute.HttpClient.Interfaces.IHttpErrorResponse.html), its [`ToException()`](~/api/Kampute.HttpClient.Interfaces.IHttpErrorResponse.html#Kampute_HttpClient_Interfaces_IHttpErrorResponse_ToException_System_Net_HttpStatusCode_) method constructs the exception. See the reference for the full error-response contract.

Expand Down
27 changes: 26 additions & 1 deletion src/Kampute.HttpClient.Json/JsonFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,29 @@ namespace Kampute.HttpClient.Json
/// Reads and writes <c>application/json</c> content with <c>System.Text.Json</c>.
/// </summary>
/// <remarks>
/// <para>
/// The formatter also reads <c>application/problem+json</c>, the problem details format of RFC 9457 for error responses, and advertises it in
/// the <c>Accept</c> header. It reads any other media type with the <c>+json</c> structured syntax suffix, which RFC 6839 permits for media
/// types whose representation follows <c>application/json</c>, such as <c>application/vnd.example+json</c>, but does not advertise it. It writes
/// <c>application/json</c> only.
/// </para>
/// <para>
/// The client reads a response with the first formatter in <see cref="HttpRestClient.ContentFormatters"/> that can read it, so a formatter for a
/// specific <c>+json</c> media type takes over that type only when it is added before this one.
/// </para>
/// <para>
/// Register the formatter with <see cref="HttpRestClientJsonExtensions.UseJson"/>. Its <see cref="Options"/> apply both to the responses it
/// reads and to the payloads it writes. Change them before the client sends requests, because a registered formatter is shared by all requests
/// of the client.
/// </para>
/// </remarks>
public sealed class JsonFormatter : HttpContentFormatter
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonFormatter"/> class.
/// </summary>
public JsonFormatter()
: base([MediaTypeNames.Application.Json], [MediaTypeNames.Application.Json])
: base([MediaTypeNames.Application.Json, MediaTypeNames.Application.ProblemJson], [MediaTypeNames.Application.Json])
{
}

Expand All @@ -39,6 +51,19 @@ public JsonFormatter()
/// </value>
public JsonSerializerOptions? Options { get; set; }

/// <summary>
/// Determines whether this formatter can read content of the specified media type.
/// </summary>
/// <param name="mediaType">The media type of the content, without parameters.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="mediaType"/> is one of the <see cref="HttpContentFormatter.ReadableMediaTypes"/> or has the
/// <c>+json</c> structured syntax suffix; otherwise, <see langword="false"/>.
/// </returns>
protected override bool CanReadMediaType(string mediaType)
{
return base.CanReadMediaType(mediaType) || HasStructuredSyntaxSuffix(mediaType, "+json");
}

/// <summary>
/// Asynchronously reads an object of the specified type from JSON content.
/// </summary>
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>3.0.0</Version>
<Version>3.1.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2025 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>3.0.0</Version>
<Version>3.1.0</Version>
<Company>Kampute</Company>
<Copyright>Copyright (c) 2025 Kampute</Copyright>
<LangVersion>latest</LangVersion>
Expand Down
27 changes: 26 additions & 1 deletion src/Kampute.HttpClient.NewtonsoftJson/NewtonsoftJsonFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,29 @@ namespace Kampute.HttpClient.NewtonsoftJson
/// Reads and writes <c>application/json</c> content with <c>Newtonsoft.Json</c>.
/// </summary>
/// <remarks>
/// <para>
/// The formatter also reads <c>application/problem+json</c>, the problem details format of RFC 9457 for error responses, and advertises it in
/// the <c>Accept</c> header. It reads any other media type with the <c>+json</c> structured syntax suffix, which RFC 6839 permits for media
/// types whose representation follows <c>application/json</c>, such as <c>application/vnd.example+json</c>, but does not advertise it. It writes
/// <c>application/json</c> only.
/// </para>
/// <para>
/// The client reads a response with the first formatter in <see cref="HttpRestClient.ContentFormatters"/> that can read it, so a formatter for a
/// specific <c>+json</c> media type takes over that type only when it is added before this one.
/// </para>
/// <para>
/// Register the formatter with <see cref="HttpRestClientJsonExtensions.UseNewtonsoftJson"/>. Its <see cref="Settings"/> apply both to the responses
/// it reads and to the payloads it writes. Change them before the client sends requests, because a registered formatter is shared by all requests
/// of the client.
/// </para>
/// </remarks>
public sealed class NewtonsoftJsonFormatter : HttpContentFormatter
{
/// <summary>
/// Initializes a new instance of the <see cref="NewtonsoftJsonFormatter"/> class.
/// </summary>
public NewtonsoftJsonFormatter()
: base([MediaTypeNames.Application.Json], [MediaTypeNames.Application.Json])
: base([MediaTypeNames.Application.Json, MediaTypeNames.Application.ProblemJson], [MediaTypeNames.Application.Json])
{
}

Expand All @@ -40,6 +52,19 @@ public NewtonsoftJsonFormatter()
/// </value>
public JsonSerializerSettings? Settings { get; set; }

/// <summary>
/// Determines whether this formatter can read content of the specified media type.
/// </summary>
/// <param name="mediaType">The media type of the content, without parameters.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="mediaType"/> is one of the <see cref="HttpContentFormatter.ReadableMediaTypes"/> or has the
/// <c>+json</c> structured syntax suffix; otherwise, <see langword="false"/>.
/// </returns>
protected override bool CanReadMediaType(string mediaType)
{
return base.CanReadMediaType(mediaType) || HasStructuredSyntaxSuffix(mediaType, "+json");
}

/// <summary>
/// Asynchronously reads an object of the specified type from JSON content.
/// </summary>
Expand Down
43 changes: 40 additions & 3 deletions src/Kampute.HttpClient/Content/Abstracts/HttpContentFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ namespace Kampute.HttpClient.Content.Abstracts
/// <para>
/// A derived class passes the media types it reads and the media types it writes to the constructor. A receive-only formatter passes no writable
/// media types and overrides <see cref="ReadContentAsync"/>; a send-only formatter passes no readable media types and overrides <see cref="CreateContent"/>;
/// a two-way formatter does both. To limit the types a formatter handles, override <see cref="CanReadType"/> or <see cref="CanWriteType"/>.
/// a two-way formatter does both. To limit the types a formatter handles, override <see cref="CanReadType"/> or <see cref="CanWriteType"/>. To read
/// media types that are not listed, such as every media type with a structured syntax suffix, override <see cref="CanReadMediaType"/>.
/// </para>
/// <para>
/// Media types are compared ignoring case. <see cref="ReadAsync"/> and <see cref="Write"/> validate their arguments before they call the
Expand Down Expand Up @@ -80,13 +81,13 @@ public virtual IEnumerable<string> GetReadableMediaTypes(Type modelType)
/// <param name="mediaType">The media type of the content.</param>
/// <param name="modelType">The type of the object to read.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="mediaType"/> is one of the <see cref="ReadableMediaTypes"/> and <see cref="CanReadType"/> accepts
/// <see langword="true"/> if <see cref="CanReadMediaType"/> accepts <paramref name="mediaType"/> and <see cref="CanReadType"/> accepts
/// <paramref name="modelType"/>; otherwise, <see langword="false"/>.
/// </returns>
public virtual bool CanRead(string mediaType, Type modelType)
{
return mediaType is not null && modelType is not null
&& ReadableMediaTypes.Contains(mediaType, StringComparer.OrdinalIgnoreCase)
&& CanReadMediaType(mediaType)
&& CanReadType(modelType);
}

Expand Down Expand Up @@ -163,6 +164,42 @@ public HttpContent Write(object payload, string mediaType)
/// <returns><see langword="true"/> if this formatter can read objects of <paramref name="modelType"/>; otherwise, <see langword="false"/>. The default is <see langword="true"/>.</returns>
protected virtual bool CanReadType(Type modelType) => true;

/// <summary>
/// Determines whether this formatter can read content of the specified media type.
/// </summary>
/// <param name="mediaType">The media type of the content, without parameters.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="mediaType"/> is one of the <see cref="ReadableMediaTypes"/>, ignoring case; otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// <see cref="CanRead"/> calls this method with a non-null media type. A media type accepted only by an override is read but not advertised
/// in the <c>Accept</c> header, which lists <see cref="ReadableMediaTypes"/>.
/// </remarks>
protected virtual bool CanReadMediaType(string mediaType) => ReadableMediaTypes.Contains(mediaType, StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Determines whether a media type ends with the specified structured syntax suffix.
/// </summary>
/// <param name="mediaType">The media type to check, such as <c>application/vnd.example+json</c>.</param>
/// <param name="suffix">The suffix, including its plus sign, such as <c>+json</c>.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="mediaType"/> has a subtype name before <paramref name="suffix"/> and ends with it, ignoring case;
/// otherwise, <see langword="false"/>.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="mediaType"/> or <paramref name="suffix"/> is <see langword="null"/>.</exception>
protected static bool HasStructuredSyntaxSuffix(string mediaType, string suffix)
{
if (mediaType is null)
throw new ArgumentNullException(nameof(mediaType));
if (suffix is null)
throw new ArgumentNullException(nameof(suffix));

var slash = mediaType.IndexOf('/');
return slash > 0
&& mediaType.Length - suffix.Length > slash + 1
&& mediaType.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Determines whether this formatter can write payloads of the specified type.
/// </summary>
Expand Down
Loading
Loading