diff --git a/Modules/Conversion/Serializers/SerializationRegistry.cs b/Modules/Conversion/Serializers/SerializationRegistry.cs
index 206d5771..ab5dd9c4 100644
--- a/Modules/Conversion/Serializers/SerializationRegistry.cs
+++ b/Modules/Conversion/Serializers/SerializationRegistry.cs
@@ -1,71 +1,81 @@
-using System.Collections.Generic;
-
-using GenHTTP.Api.Protocol;
-
-namespace GenHTTP.Modules.Conversion.Providers
-{
-
- ///
- /// Registers formats that can be used to serialize and
- /// deserialize objects sent to or received from a
- /// service oriented handler.
- ///
- public sealed class SerializationRegistry
- {
-
- #region Get-/Setters
-
- private FlexibleContentType Default { get; }
-
- private Dictionary Formats { get; }
-
- #endregion
-
- #region Initialization
-
- public SerializationRegistry(FlexibleContentType defaultType,
- Dictionary formats)
- {
- Default = defaultType;
- Formats = formats;
- }
-
- #endregion
-
- #region Functionality
-
- public ISerializationFormat? GetDeserialization(IRequest request)
- {
- if (request.Headers.TryGetValue("Content-Type", out string? requested))
- {
- return GetFormat(FlexibleContentType.Parse(requested));
- }
-
- return GetFormat(Default);
- }
-
- public ISerializationFormat? GetSerialization(IRequest request)
- {
- if (request.Headers.TryGetValue("Accept", out string? accepted))
- {
- return GetFormat(FlexibleContentType.Parse(accepted)) ?? GetFormat(Default);
- }
-
- return GetFormat(Default);
- }
-
- private ISerializationFormat? GetFormat(FlexibleContentType contentType)
- {
- if (Formats.TryGetValue(contentType, out var format))
- {
- return format;
- }
-
- return null;
- }
-
- #endregion
-
- }
-
-}
+using System.Collections.Generic;
+
+using GenHTTP.Api.Protocol;
+
+namespace GenHTTP.Modules.Conversion.Providers
+{
+
+ ///
+ /// Registers formats that can be used to serialize and
+ /// deserialize objects sent to or received from a
+ /// service oriented handler.
+ ///
+ public sealed class SerializationRegistry
+ {
+
+ #region Get-/Setters
+
+ private FlexibleContentType Default { get; }
+
+ private Dictionary Formats { get; }
+
+ #endregion
+
+ #region Initialization
+
+ public SerializationRegistry(FlexibleContentType defaultType,
+ Dictionary formats)
+ {
+ Default = defaultType;
+ Formats = formats;
+ }
+
+ #endregion
+
+ #region Functionality
+
+ public ISerializationFormat? GetDeserialization(IRequest request)
+ {
+ if (request.Headers.TryGetValue("Content-Type", out string? requested))
+ {
+ return GetFormat(FlexibleContentType.Parse(requested));
+ }
+
+ return GetFormat(Default);
+ }
+
+ public ISerializationFormat? GetSerialization(IRequest request)
+ {
+ if (request.Headers.TryGetValue("Accept", out string? accepted))
+ {
+ return GetFormat(FlexibleContentType.Parse(accepted)) ?? GetFormat(Default);
+ }
+
+ return GetFormat(Default);
+ }
+
+ public ISerializationFormat? GetFormat(string? contentType)
+ {
+ if (contentType != null)
+ {
+ return GetFormat(FlexibleContentType.Parse(contentType));
+ }
+
+ return GetFormat(Default);
+ }
+
+ private ISerializationFormat? GetFormat(FlexibleContentType contentType)
+ {
+ if (Formats.TryGetValue(contentType, out var format))
+ {
+ return format;
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ }
+
+}
diff --git a/Testing/Acceptance/Testing/ContentTests.cs b/Testing/Acceptance/Testing/ContentTests.cs
new file mode 100644
index 00000000..0f41b058
--- /dev/null
+++ b/Testing/Acceptance/Testing/ContentTests.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Threading.Tasks;
+using GenHTTP.Api.Protocol;
+using GenHTTP.Modules.Functional;
+using GenHTTP.Modules.Reflection;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace GenHTTP.Testing.Acceptance.Testing
+{
+
+ [TestClass]
+ public sealed class ContentTests
+ {
+
+ public record class MyType(int ID);
+
+ [TestMethod]
+ public async Task TestDeserialization()
+ {
+ var expectation = new MyType(42);
+
+ var handler = Inline.Create()
+ .Get(() => expectation);
+
+ using var host = TestHost.Run(handler);
+
+ using var response = await host.GetResponseAsync();
+
+ Assert.AreEqual(expectation, await response.GetContentAsync());
+ }
+
+ [TestMethod]
+ public async Task TestNull()
+ {
+ var handler = Inline.Create()
+ .Get(() => (MyType?)null);
+
+ using var host = TestHost.Run(handler);
+
+ using var response = await host.GetResponseAsync();
+
+ Assert.IsNull(await response.GetOptionalContentAsync());
+ }
+
+ [TestMethod]
+ public async Task TestUnsupported()
+ {
+ var handler = Inline.Create()
+ .Get(() => new Result("Nah").Type(FlexibleContentType.Get("text/html")));
+
+ using var host = TestHost.Run(handler);
+
+ using var response = await host.GetResponseAsync();
+
+ await Assert.ThrowsExceptionAsync(async () =>
+ {
+ await response.GetOptionalContentAsync();
+ });
+ }
+
+ }
+
+}
diff --git a/Testing/Testing/ContentExtensions.cs b/Testing/Testing/ContentExtensions.cs
new file mode 100644
index 00000000..990d4688
--- /dev/null
+++ b/Testing/Testing/ContentExtensions.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+using GenHTTP.Modules.Conversion;
+using GenHTTP.Modules.Protobuf;
+
+namespace GenHTTP.Testing
+{
+
+ public static class ContentExtensions
+ {
+
+ ///
+ /// Reads the response body as a string.
+ ///
+ /// The response to read
+ /// The content of the HTTP response
+ public static async ValueTask GetContentAsync(this HttpResponseMessage response) => await response.Content.ReadAsStringAsync();
+
+ ///
+ /// Deserializes the payload of the HTTP response into the given type.
+ ///
+ /// The type of the payload to be read
+ /// The response to read the payload from
+ /// The deserialized payload of the response
+ /// Thrown if the format returned by the server is not supported
+ ///
+ /// This method supports all formats that ship with the GenHTTP framework (JSON, XML, form encoded, Protobuf)
+ /// and falls back to JSON if the server does not indicate a content type.
+ ///
+ public static async ValueTask GetContentAsync(this HttpResponseMessage response)
+ {
+ return await response.GetOptionalContentAsync() ?? throw new InvalidOperationException("Server did not return a result");
+ }
+
+ ///
+ /// Attempts to deserialize the payload of the HTTP response into the given type.
+ ///
+ /// The type of the payload to be read
+ /// The response to read the payload from
+ /// The deserialized payload of the response or null, if the server did not return data
+ /// Thrown if the format returned by the server is not supported
+ ///
+ /// This method supports all formats that ship with the GenHTTP framework (JSON, XML, form encoded, Protobuf)
+ /// and falls back to JSON if the server does not indicate a content type.
+ ///
+ public static async ValueTask GetOptionalContentAsync(this HttpResponseMessage response)
+ {
+ if (response.StatusCode == HttpStatusCode.NoContent)
+ {
+ return default;
+ }
+
+ var type = response.GetContentHeader("Content-Type");
+
+ var registry = Serialization.Default()
+ .AddProtobuf()
+ .Build();
+
+ var format = registry.GetFormat(type) ?? throw new InvalidOperationException($"Unable to find deserializer for content type '{type}'");
+
+ using var body = await response.Content.ReadAsStreamAsync();
+
+ return (T?)await format.DeserializeAsync(body, typeof(T));
+ }
+
+ }
+
+}
diff --git a/Testing/Testing/GenHTTP.Testing.csproj b/Testing/Testing/GenHTTP.Testing.csproj
index 1a5e77ae..d40c8c24 100644
--- a/Testing/Testing/GenHTTP.Testing.csproj
+++ b/Testing/Testing/GenHTTP.Testing.csproj
@@ -48,6 +48,8 @@
+
+
diff --git a/Testing/Testing/TestExtensions.cs b/Testing/Testing/HeaderExtensions.cs
similarity index 60%
rename from Testing/Testing/TestExtensions.cs
rename to Testing/Testing/HeaderExtensions.cs
index 9844c72d..bce03541 100644
--- a/Testing/Testing/TestExtensions.cs
+++ b/Testing/Testing/HeaderExtensions.cs
@@ -1,40 +1,32 @@
-using System.Linq;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace GenHTTP.Testing
-{
-
- public static class TestExtensions
- {
-
- ///
- /// Reads the response body as a string.
- ///
- /// The response to read
- /// The content of the HTTP response
- public static async ValueTask GetContentAsync(this HttpResponseMessage response) => await response.Content.ReadAsStringAsync();
-
- public static string? GetHeader(this HttpResponseMessage response, string key)
- {
- if (response.Headers.TryGetValues(key, out var values))
- {
- return values.FirstOrDefault();
- }
-
- return null;
- }
-
- public static string? GetContentHeader(this HttpResponseMessage response, string key)
- {
- if (response.Content.Headers.TryGetValues(key, out var values))
- {
- return values.FirstOrDefault();
- }
-
- return null;
- }
-
- }
-
-}
+using System.Linq;
+using System.Net.Http;
+
+namespace GenHTTP.Testing
+{
+
+ public static class HeaderExtensions
+ {
+
+ public static string? GetHeader(this HttpResponseMessage response, string key)
+ {
+ if (response.Headers.TryGetValues(key, out var values))
+ {
+ return values.FirstOrDefault();
+ }
+
+ return null;
+ }
+
+ public static string? GetContentHeader(this HttpResponseMessage response, string key)
+ {
+ if (response.Content.Headers.TryGetValues(key, out var values))
+ {
+ return values.FirstOrDefault();
+ }
+
+ return null;
+ }
+
+ }
+
+}