Skip to content

Commit 7c8b3d0

Browse files
committed
Add IResponseType interface and enhance response serialization
1 parent 8e01ac5 commit 7c8b3d0

File tree

4 files changed

+74
-12
lines changed

4 files changed

+74
-12
lines changed

src/Arbiter.CommandQuery/Commands/PrincipalCommandBase.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ namespace Arbiter.CommandQuery.Commands;
3333
/// Console.WriteLine($"User Name: {result?.Name}");
3434
/// </code>
3535
/// </example>
36-
public abstract record PrincipalCommandBase<TResponse> : IRequest<TResponse>, IRequestPrincipal
36+
public abstract record PrincipalCommandBase<TResponse> : IRequest<TResponse>, IRequestPrincipal, IResponseType
3737
{
3838
/// <summary>
3939
/// Initializes a new instance of the <see cref="PrincipalCommandBase{TResponse}"/> class.
@@ -47,6 +47,7 @@ protected PrincipalCommandBase(ClaimsPrincipal? principal)
4747
ActivatedBy = principal?.Identity?.Name ?? "system";
4848
}
4949

50+
5051
/// <summary>
5152
/// Gets the <see cref="ClaimsPrincipal"/> representing the user executing the command.
5253
/// </summary>
@@ -82,6 +83,7 @@ protected PrincipalCommandBase(ClaimsPrincipal? principal)
8283
[IgnoreMember]
8384
public string? ActivatedBy { get; private set; }
8485

86+
8587
/// <summary>
8688
/// Applies the specified <see cref="ClaimsPrincipal"/> to the command.
8789
/// </summary>
@@ -92,4 +94,9 @@ void IRequestPrincipal.ApplyPrincipal(ClaimsPrincipal? principal)
9294
Activated = DateTimeOffset.UtcNow;
9395
ActivatedBy = principal?.Identity?.Name ?? "system";
9496
}
97+
98+
/// <summary>
99+
/// Gets the type of the response returned by the command.
100+
/// </summary>
101+
Type IResponseType.ResponseType => typeof(TResponse);
95102
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Arbiter.CommandQuery.Definitions;
2+
3+
/// <summary>
4+
/// Defines a contract for types that declare their response type.
5+
/// </summary>
6+
public interface IResponseType
7+
{
8+
/// <summary>
9+
/// Gets the type of the response.
10+
/// </summary>
11+
abstract Type ResponseType { get; }
12+
}

src/Arbiter.Dispatcher.Server/DispatcherEndpoint.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,14 @@ private async Task<IResult> Send(
111111
return Results.Empty;
112112
}
113113

114+
// Determine response type if available, used for serialization
115+
Type? responseType = null;
116+
if (response is IResponseType responseInstance)
117+
responseType = responseInstance.ResponseType;
118+
114119
return isJson
115120
? TypedResults.Json<object>(response)
116-
: new MessagePackResult<object>(response);
121+
: new MessagePackResult(response, valueType: responseType);
117122
}
118123
catch (Exception ex)
119124
{

src/Arbiter.Dispatcher.Server/MessagePackResult.cs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,48 @@ namespace Arbiter.Dispatcher.Server;
1515
/// <summary>
1616
/// An <see cref="IResult"/> that serializes a value to MessagePack format and writes it to the HTTP response.
1717
/// </summary>
18-
/// <typeparam name="TValue">The type of the value to serialize.</typeparam>
19-
public class MessagePackResult<TValue> :
18+
/// <remarks>
19+
/// <para>
20+
/// This class provides a way to return MessagePack-serialized responses from ASP.NET Core minimal API endpoints.
21+
/// MessagePack is a binary serialization format that is typically faster and more compact than JSON.
22+
/// </para>
23+
/// <para>
24+
/// The class automatically handles null values by returning a 204 No Content response, and supports
25+
/// explicit type specification for serialization scenarios where the runtime type differs from the desired
26+
/// serialization type. This is particularly useful with collection initializers or when serializing
27+
/// derived types as their base type or interface.
28+
/// </para>
29+
/// <para>
30+
/// When used in minimal API endpoints, this class implements <see cref="IEndpointMetadataProvider"/>
31+
/// to automatically populate OpenAPI metadata for proper API documentation.
32+
/// </para>
33+
/// </remarks>
34+
public class MessagePackResult :
2035
IResult,
2136
IValueHttpResult,
22-
IValueHttpResult<TValue>,
2337
IStatusCodeHttpResult,
2438
IContentTypeHttpResult,
2539
IEndpointMetadataProvider
2640
{
2741
/// <summary>
28-
/// Initializes a new instance of the <see cref="MessagePackResult{TValue}"/> class.
42+
/// Initializes a new instance of the <see cref="MessagePackResult"/> class.
2943
/// </summary>
3044
/// <param name="value">The value to serialize. If <c>null</c>, a 204 No Content response is returned.</param>
3145
/// <param name="statusCode">The HTTP status code. If not specified, defaults to 200 OK for non-null values and 204 No Content for null values.</param>
3246
/// <param name="contentType">The content type. If not specified, defaults to the MessagePack content type.</param>
33-
public MessagePackResult(TValue? value, int? statusCode = null, string? contentType = null)
47+
/// <param name="valueType">The type to use for MessagePack serialization. If not specified, defaults to the runtime type of the value.</param>
48+
/// <remarks>
49+
/// The <paramref name="valueType"/> parameter is used to explicitly specify the type for serialization.
50+
/// This is particularly useful when the runtime type differs from the desired serialization type.
51+
/// A common scenario is with collection initializers, where the concrete type (e.g., <c>List&lt;T&gt;</c>)
52+
/// may need to be serialized as a different type (e.g., <c>IEnumerable&lt;T&gt;</c> or <c>T[]</c>).
53+
/// </remarks>
54+
public MessagePackResult(object? value, int? statusCode = null, string? contentType = null, Type? valueType = null)
3455
{
3556
Value = value;
3657
StatusCode = statusCode;
3758
ContentType = contentType;
59+
ValueType = valueType;
3860
}
3961

4062
/// <summary>
@@ -43,10 +65,7 @@ public MessagePackResult(TValue? value, int? statusCode = null, string? contentT
4365
/// <value>
4466
/// The value to serialize, or <c>null</c> if no content should be returned.
4567
/// </value>
46-
public TValue? Value { get; }
47-
48-
/// <inheritdoc/>
49-
object? IValueHttpResult.Value => Value;
68+
public object? Value { get; }
5069

5170
/// <summary>
5271
/// Gets the content type for the response.
@@ -64,6 +83,25 @@ public MessagePackResult(TValue? value, int? statusCode = null, string? contentT
6483
/// </value>
6584
public int? StatusCode { get; }
6685

86+
/// <summary>
87+
/// Gets the type to use for MessagePack serialization.
88+
/// </summary>
89+
/// <value>
90+
/// The type to use for serialization, or <c>null</c> to use the runtime type of <see cref="Value"/>.
91+
/// </value>
92+
/// <remarks>
93+
/// <para>
94+
/// This property allows explicit specification of the serialization type, which is useful when
95+
/// the value should be serialized as a base type or interface rather than its concrete type.
96+
/// If not specified, the actual runtime type of the value is used.
97+
/// </para>
98+
/// <para>
99+
/// The type is used both for MessagePack serialization and to generate a portable type name
100+
/// that is added to the response headers via <see cref="DispatcherConstants.ResponseTypeHeader"/>.
101+
/// This portable name helps clients properly deserialize the response by providing explicit type information.
102+
/// </para>
103+
/// </remarks>
104+
public Type? ValueType { get; }
67105

68106
/// <summary>
69107
/// Executes the result operation, serializing the value to MessagePack format and writing it to the HTTP response.
@@ -86,7 +124,7 @@ public async Task ExecuteAsync(HttpContext httpContext)
86124
if (StatusCode is { } statusCode)
87125
httpContext.Response.StatusCode = statusCode;
88126

89-
var valueType = Value.GetType();
127+
var valueType = ValueType ?? Value.GetType();
90128
var responseType = valueType.GetPortableName();
91129

92130
var options = httpContext.RequestServices.GetService<MessagePackSerializerOptions>()

0 commit comments

Comments
 (0)