Skip to content

Commit 5c7623e

Browse files
committed
Refactor dispatcher to support JSON and MessagePack clients
1 parent 21a24a6 commit 5c7623e

File tree

13 files changed

+767
-339
lines changed

13 files changed

+767
-339
lines changed

Directory.Packages.props

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<PackageVersion Include="Azure.Communication.Email" Version="1.1.0" />
1414
<PackageVersion Include="Azure.Communication.Sms" Version="1.0.2" />
1515
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
16-
<PackageVersion Include="Blazilla" Version="2.2.0" />
16+
<PackageVersion Include="Blazilla" Version="2.2.1" />
1717
<PackageVersion Include="Bogus" Version="35.6.5" />
1818
<PackageVersion Include="CsvHelper" Version="33.1.0" />
1919
<PackageVersion Include="dbup-sqlserver" Version="6.0.16" />
@@ -58,7 +58,7 @@
5858
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.11.0-beta.2" />
5959
<PackageVersion Include="Riok.Mapperly" Version="4.3.1" />
6060
<PackageVersion Include="Rocks" Version="10.0.0" />
61-
<PackageVersion Include="Scalar.AspNetCore" Version="2.12.8" />
61+
<PackageVersion Include="Scalar.AspNetCore" Version="2.12.10" />
6262
<PackageVersion Include="SendGrid.Extensions.DependencyInjection" Version="1.0.1" />
6363
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
6464
<PackageVersion Include="starkbank-ecdsa" Version="1.3.3" />
@@ -69,9 +69,9 @@
6969
<PackageVersion Include="TestHost.Abstracts" Version="2.0.0" />
7070
<PackageVersion Include="Testcontainers.MongoDb" Version="4.10.0" />
7171
<PackageVersion Include="Testcontainers.MsSql" Version="4.10.0" />
72-
<PackageVersion Include="TUnit" Version="1.11.45" />
72+
<PackageVersion Include="TUnit" Version="1.11.56" />
7373
<PackageVersion Include="Twilio" Version="7.14.1" />
7474
<PackageVersion Include="Verify.TUnit" Version="31.9.4" />
7575
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
7676
</ItemGroup>
77-
</Project>
77+
</Project>

samples/EntityFramework/src/Tracker.Client/Services/ServiceRegistration.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static void Register(IServiceCollection services, ISet<string> tags)
3434
if (tags.Contains("WebAssembly"))
3535
{
3636
services
37-
.AddRemoteDispatcher((sp, client) =>
37+
.AddMessagePackDispatcher((sp, client) =>
3838
{
3939
var hostEnvironment = sp.GetRequiredService<IOptions<EnvironmentOptions>>();
4040
client.BaseAddress = new Uri(hostEnvironment.Value.BaseAddress);

src/Arbiter.CommandQuery/CommandQueryExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,26 @@ public static IServiceCollection AddEntityHybridCache(this IServiceCollection se
8181
return services;
8282
}
8383

84+
/// <summary>
85+
/// Adds MessagePack options to the service collection.
86+
/// </summary>
87+
/// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
88+
/// <param name="configure">An optional action to configure the MessagePack serializer options.</param>
89+
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
90+
/// <seealso cref="MessagePackDefaults"/>
91+
public static IServiceCollection AddMessagePackOptions(this IServiceCollection services, Action<MessagePack.MessagePackSerializerOptions>? configure = null)
92+
{
93+
ArgumentNullException.ThrowIfNull(services);
94+
95+
// Configure MessagePack options
96+
var options = MessagePackDefaults.DefaultSerializerOptions;
97+
configure?.Invoke(options);
98+
99+
// MessagePack Serializer Options Registration
100+
services.TryAddSingleton(options);
101+
102+
return services;
103+
}
84104

85105
/// <summary>
86106
/// Adds the entity query behaviors to the service collection.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using MessagePack;
2+
using MessagePack.Resolvers;
3+
4+
namespace Arbiter.CommandQuery;
5+
6+
/// <summary>
7+
/// Provides default values and settings for MessagePack serialization.
8+
/// </summary>
9+
public static class MessagePackDefaults
10+
{
11+
/// <summary>
12+
/// The content type for MessagePack serialized data.
13+
/// </summary>
14+
public const string MessagePackContentType = "application/x-msgpack";
15+
16+
/// <summary>
17+
/// The default serializer options for MessagePack.
18+
/// </summary>
19+
public static readonly MessagePackSerializerOptions DefaultSerializerOptions =
20+
MessagePackSerializerOptions.Standard
21+
.WithResolver(
22+
CompositeResolver.Create(
23+
[
24+
// 0) Uses the Roslyn source generator output produced automatically.
25+
SourceGeneratedFormatterResolver.Instance,
26+
27+
// 1) Attribute-based + built-ins (enums, primitives, etc.)
28+
StandardResolver.Instance,
29+
30+
// 2) Typeless (for object-typed or unknown static types)
31+
TypelessObjectResolver.Instance,
32+
33+
// 3) Contractless fallback for POCOs without attributes
34+
ContractlessStandardResolver.Instance,
35+
])
36+
)
37+
.WithCompression(MessagePackCompression.Lz4Block);
38+
39+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Buffers;
2+
using System.Net.Http.Headers;
3+
using System.Net.Mime;
4+
using System.Text.Json;
5+
6+
using Arbiter.CommandQuery.Definitions;
7+
using Arbiter.CommandQuery.Models;
8+
using Arbiter.Mediation;
9+
10+
using Microsoft.Extensions.Caching.Hybrid;
11+
12+
namespace Arbiter.Dispatcher.Client;
13+
14+
/// <summary>
15+
/// Implements a remote dispatcher that sends requests to a remote HTTP endpoint using JSON serialization.
16+
/// Supports optional hybrid caching for cacheable requests.
17+
/// </summary>
18+
public class JsonDispatcher : RemoteDispatcherBase
19+
{
20+
private static readonly MediaTypeHeaderValue _mediaTypeHeader = new(MediaTypeNames.Application.Json);
21+
private readonly JsonSerializerOptions _options;
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="JsonDispatcher"/> class.
25+
/// </summary>
26+
/// <param name="httpClient">The HTTP client used to send requests to the remote endpoint.</param>
27+
/// <param name="options">The JSON serialization options.</param>
28+
/// <param name="hybridCache">Optional hybrid cache for caching responses. When provided, requests implementing <see cref="ICacheResult"/> will be cached.</param>
29+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="httpClient"/> or <paramref name="options"/> is null.</exception>
30+
public JsonDispatcher(HttpClient httpClient, JsonSerializerOptions options, HybridCache? hybridCache = null)
31+
: base(httpClient, hybridCache)
32+
{
33+
ArgumentNullException.ThrowIfNull(options);
34+
35+
_options = options;
36+
}
37+
38+
/// <inheritdoc/>
39+
protected override MediaTypeHeaderValue ContentType { get; } = _mediaTypeHeader;
40+
41+
/// <inheritdoc/>
42+
protected override void Serialize<TResponse>(
43+
IBufferWriter<byte> bufferWriter,
44+
IRequest<TResponse> request,
45+
Type requestType,
46+
CancellationToken cancellationToken)
47+
{
48+
using var writer = new Utf8JsonWriter(bufferWriter);
49+
JsonSerializer.Serialize(writer, request, requestType, _options);
50+
}
51+
52+
/// <inheritdoc/>
53+
protected override TResponse? Deserialize<TResponse>(
54+
ReadOnlyMemory<byte> buffer,
55+
CancellationToken cancellationToken)
56+
where TResponse : default
57+
{
58+
return JsonSerializer.Deserialize<TResponse>(buffer.Span, _options);
59+
}
60+
61+
/// <inheritdoc/>
62+
protected override async ValueTask<ProblemDetails?> TryProblemDetails(
63+
HttpResponseMessage responseMessage,
64+
CancellationToken cancellationToken = default)
65+
{
66+
// check content type is JSON
67+
var mediaType = responseMessage.Content.Headers.ContentType?.MediaType;
68+
if (!string.Equals(mediaType, MediaTypeNames.Application.ProblemJson, StringComparison.OrdinalIgnoreCase))
69+
return null;
70+
71+
// deserialize problem details from buffer
72+
var responseBytes = await responseMessage.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
73+
return JsonSerializer.Deserialize<ProblemDetails>(responseBytes, _options);
74+
}
75+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System.Buffers;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Net;
4+
using System.Net.Http.Headers;
5+
using System.Net.Http.Json;
6+
using System.Net.Mime;
7+
using System.Text.Json;
8+
9+
using Arbiter.CommandQuery;
10+
using Arbiter.CommandQuery.Definitions;
11+
using Arbiter.CommandQuery.Extensions;
12+
using Arbiter.CommandQuery.Models;
13+
using Arbiter.Mediation;
14+
15+
using MessagePack;
16+
17+
using Microsoft.Extensions.Caching.Hybrid;
18+
19+
namespace Arbiter.Dispatcher.Client;
20+
21+
/// <summary>
22+
/// Implements a remote dispatcher that sends requests to a remote HTTP endpoint using MessagePack serialization.
23+
/// Supports optional hybrid caching for cacheable requests.
24+
/// </summary>
25+
public class MessagePackDispatcher : RemoteDispatcherBase
26+
{
27+
private readonly MessagePackSerializerOptions _options;
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="MessagePackDispatcher"/> class.
31+
/// </summary>
32+
/// <param name="httpClient">The HTTP client used to send requests to the remote endpoint.</param>
33+
/// <param name="options">The MessagePack serialization options.</param>
34+
/// <param name="hybridCache">Optional hybrid cache for caching responses. When provided, requests implementing <see cref="ICacheResult"/> will be cached.</param>
35+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="httpClient"/> or <paramref name="options"/> is null.</exception>
36+
public MessagePackDispatcher(
37+
HttpClient httpClient,
38+
MessagePackSerializerOptions options,
39+
HybridCache? hybridCache = null) : base(httpClient, hybridCache)
40+
{
41+
ArgumentNullException.ThrowIfNull(options);
42+
43+
_options = options;
44+
}
45+
46+
/// <inheritdoc/>
47+
protected override MediaTypeHeaderValue ContentType { get; } = DispatcherConstants.MessagePackMediaTypeHeader;
48+
49+
/// <inheritdoc/>
50+
protected override void Serialize<TResponse>(IBufferWriter<byte> bufferWriter, IRequest<TResponse> request, Type requestType, CancellationToken cancellationToken)
51+
{
52+
MessagePackSerializer.Serialize(requestType, bufferWriter, request, _options);
53+
}
54+
55+
/// <inheritdoc/>
56+
protected override TResponse? Deserialize<TResponse>(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
57+
where TResponse : default
58+
{
59+
return MessagePackSerializer.Deserialize<TResponse>(buffer, _options, cancellationToken);
60+
}
61+
62+
/// <inheritdoc/>
63+
protected override async ValueTask<ProblemDetails?> TryProblemDetails(HttpResponseMessage responseMessage, CancellationToken cancellationToken = default)
64+
{
65+
// check content type is MessagePack
66+
var mediaType = responseMessage.Content.Headers.ContentType?.MediaType;
67+
if (!string.Equals(mediaType, MessagePackDefaults.MessagePackContentType, StringComparison.OrdinalIgnoreCase))
68+
return null;
69+
70+
// make sure it's a problem details response
71+
responseMessage.Headers.TryGetValues(DispatcherConstants.ResponseTypeHeader, out var responseTypeValues);
72+
var responseType = responseTypeValues?.FirstOrDefault();
73+
if (string.IsNullOrEmpty(responseType) || !string.Equals(responseType, typeof(ProblemDetails).GetPortableName(), StringComparison.OrdinalIgnoreCase))
74+
return null;
75+
76+
// deserialize problem details from buffer
77+
var responseBytes = await responseMessage.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
78+
return MessagePackSerializer.Deserialize<ProblemDetails>(responseBytes, _options, cancellationToken);
79+
}
80+
}

0 commit comments

Comments
 (0)