Skip to content

Commit 3ad41d2

Browse files
committed
Add AOT support, annotations, and AOT-safe JSON overloads
1 parent 7d4218d commit 3ad41d2

7 files changed

Lines changed: 94 additions & 1 deletion

File tree

src/Arbiter.Mapping/Arbiter.Mapping.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PropertyGroup>
44
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
55
<Description>Source-generated, compile-time object mapping with support for custom property expressions and IQueryable projection.</Description>
6+
<IsAotCompatible>true</IsAotCompatible>
67
</PropertyGroup>
78

89
<ItemGroup>

src/Arbiter.Mediation/Arbiter.Mediation.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
55
<Description>Simple mediator pattern implementation in .NET</Description>
6+
<IsAotCompatible>true</IsAotCompatible>
67
</PropertyGroup>
78

89
<ItemGroup>

src/Arbiter.Mediation/IMediator.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ namespace Arbiter.Mediation;
5454
/// </example>
5555
public interface IMediator
5656
{
57+
private const string DynamicSendRequiresDynamicCode = "Non-generic mediator send requires runtime generic type construction. Use Send<TRequest, TResponse> when publishing with Native AOT.";
58+
private const string DynamicSendRequiresUnreferencedCode = "Non-generic mediator send requires runtime type inspection. Use Send<TRequest, TResponse> when publishing a trimmed application.";
59+
5760
/// <summary>
5861
/// Sends a request to the appropriate handler and returns the response.
5962
/// </summary>
@@ -76,6 +79,7 @@ public interface IMediator
7679
/// <param name="cancellationToken">Cancellation token.</param>
7780
/// <returns>Awaitable task returning the <typeparamref name="TResponse"/>.</returns>
7881
/// <exception cref="ArgumentNullException">Thrown when <paramref name="request"/> is null.</exception>
82+
[RequiresDynamicCode(DynamicSendRequiresDynamicCode)]
7983
ValueTask<TResponse?> Send<TResponse>(
8084
IRequest<TResponse> request,
8185
CancellationToken cancellationToken = default);
@@ -88,6 +92,8 @@ public interface IMediator
8892
/// <returns>Awaitable task returning the handler response.</returns>
8993
/// <exception cref="ArgumentNullException">Thrown when <paramref name="request"/> is null.</exception>
9094
/// <exception cref="InvalidOperationException">Thrown when <paramref name="request"/> does not implement <see cref="IRequest{TResponse}"/> interface.</exception>
95+
[RequiresDynamicCode(DynamicSendRequiresDynamicCode)]
96+
[RequiresUnreferencedCode(DynamicSendRequiresUnreferencedCode)]
9197
ValueTask<object?> Send(
9298
object request,
9399
CancellationToken cancellationToken = default);

src/Arbiter.Mediation/Mediator.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Concurrent;
22
using System.Diagnostics;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Diagnostics.Metrics;
45

56
using Microsoft.Extensions.DependencyInjection;
@@ -11,6 +12,9 @@ namespace Arbiter.Mediation;
1112
/// </summary>
1213
public sealed class Mediator : IMediator
1314
{
15+
private const string DynamicSendRequiresDynamicCode = "Non-generic mediator send requires runtime generic type construction. Use Send<TRequest, TResponse> when publishing with Native AOT.";
16+
private const string DynamicSendRequiresUnreferencedCode = "Non-generic mediator send requires runtime type inspection. Use Send<TRequest, TResponse> when publishing a trimmed application.";
17+
1418
private static readonly ConcurrentDictionary<Type, IHandler> _handlerCache = new();
1519

1620
private readonly IServiceProvider _serviceProvider;
@@ -93,6 +97,7 @@ public Mediator(IServiceProvider serviceProvider)
9397
}
9498

9599
/// <inheritdoc />
100+
[RequiresDynamicCode(DynamicSendRequiresDynamicCode)]
96101
public async ValueTask<TResponse?> Send<TResponse>(
97102
IRequest<TResponse> request,
98103
CancellationToken cancellationToken = default)
@@ -147,6 +152,8 @@ public Mediator(IServiceProvider serviceProvider)
147152
}
148153

149154
/// <inheritdoc />
155+
[RequiresDynamicCode(DynamicSendRequiresDynamicCode)]
156+
[RequiresUnreferencedCode(DynamicSendRequiresUnreferencedCode)]
150157
public async ValueTask<object?> Send(
151158
object request,
152159
CancellationToken cancellationToken = default)

src/Arbiter.Services/Arbiter.Services.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PropertyGroup>
44
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
55
<Description>Arbiter utility services library providing CSV parsing, encryption, caching, token management, and URL building</Description>
6+
<IsAotCompatible>true</IsAotCompatible>
67
</PropertyGroup>
78

89
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">

src/Arbiter.Services/QueryStringEncoder.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System.Buffers.Text;
2+
using System.Diagnostics.CodeAnalysis;
23
using System.IO.Compression;
34
using System.Text.Json;
5+
using System.Text.Json.Serialization.Metadata;
46

57
namespace Arbiter.Services;
68

@@ -9,13 +11,18 @@ namespace Arbiter.Services;
911
/// </summary>
1012
public static class QueryStringEncoder
1113
{
14+
private const string JsonSerializerOptionsRequiresDynamicCode = "JSON serialization with JsonSerializerOptions might require runtime code generation. Use the JsonTypeInfo<T> overload for Native AOT applications.";
15+
private const string JsonSerializerOptionsRequiresUnreferencedCode = "JSON serialization with JsonSerializerOptions might require types that cannot be statically analyzed. Use the JsonTypeInfo<T> overload for trimmed applications.";
16+
1217
/// <summary>
1318
/// Encodes a value to a query string format.
1419
/// </summary>
1520
/// <typeparam name="T">The type of the value</typeparam>
1621
/// <param name="value">The value to encode</param>
1722
/// <param name="options">The JSON options to use for serialization</param>
1823
/// <returns>A query string encode value</returns>
24+
[RequiresDynamicCode(JsonSerializerOptionsRequiresDynamicCode)]
25+
[RequiresUnreferencedCode(JsonSerializerOptionsRequiresUnreferencedCode)]
1926
public static string? Encode<T>(T value, JsonSerializerOptions? options = null)
2027
{
2128
if (value is null)
@@ -34,13 +41,40 @@ public static class QueryStringEncoder
3441
return Base64Url.EncodeToString(jsonBytes);
3542
}
3643

44+
/// <summary>
45+
/// Encodes a value to a query string format using source-generated JSON metadata.
46+
/// </summary>
47+
/// <typeparam name="T">The type of the value</typeparam>
48+
/// <param name="value">The value to encode</param>
49+
/// <param name="jsonTypeInfo">The source-generated JSON metadata to use for serialization</param>
50+
/// <returns>A query string encode value</returns>
51+
public static string? Encode<T>(T value, JsonTypeInfo<T> jsonTypeInfo)
52+
{
53+
ArgumentNullException.ThrowIfNull(jsonTypeInfo);
54+
55+
if (value is null)
56+
return null;
57+
58+
using var outputStream = new MemoryStream();
59+
using var compressionStream = new BrotliStream(outputStream, CompressionLevel.Optimal);
60+
using var jsonWriter = new Utf8JsonWriter(compressionStream);
61+
62+
JsonSerializer.Serialize(jsonWriter, value, jsonTypeInfo);
63+
64+
var jsonBytes = outputStream.ToArray();
65+
66+
return Base64Url.EncodeToString(jsonBytes);
67+
}
68+
3769
/// <summary>
3870
/// Decodes a query string value to its original format.
3971
/// </summary>
4072
/// <typeparam name="T">The type of the value</typeparam>
4173
/// <param name="encodedQueryString">The encoded query string format</param>
4274
/// <param name="options">The JSON options to use for serialization</param>
4375
/// <returns>The instance decoded from the query string format</returns>
76+
[RequiresDynamicCode(JsonSerializerOptionsRequiresDynamicCode)]
77+
[RequiresUnreferencedCode(JsonSerializerOptionsRequiresUnreferencedCode)]
4478
public static T? Decode<T>(string? encodedQueryString, JsonSerializerOptions? options = null)
4579
{
4680
if (string.IsNullOrWhiteSpace(encodedQueryString))
@@ -55,4 +89,26 @@ public static class QueryStringEncoder
5589

5690
return JsonSerializer.Deserialize<T>(compressionStream, options);
5791
}
92+
93+
/// <summary>
94+
/// Decodes a query string value to its original format using source-generated JSON metadata.
95+
/// </summary>
96+
/// <typeparam name="T">The type of the value</typeparam>
97+
/// <param name="encodedQueryString">The encoded query string format</param>
98+
/// <param name="jsonTypeInfo">The source-generated JSON metadata to use for serialization</param>
99+
/// <returns>The instance decoded from the query string format</returns>
100+
public static T? Decode<T>(string? encodedQueryString, JsonTypeInfo<T> jsonTypeInfo)
101+
{
102+
ArgumentNullException.ThrowIfNull(jsonTypeInfo);
103+
104+
if (string.IsNullOrWhiteSpace(encodedQueryString))
105+
return default;
106+
107+
var jsonBytes = Base64Url.DecodeFromChars(encodedQueryString);
108+
109+
using var inputStream = new MemoryStream(jsonBytes);
110+
using var compressionStream = new BrotliStream(inputStream, CompressionMode.Decompress);
111+
112+
return JsonSerializer.Deserialize(compressionStream, jsonTypeInfo);
113+
}
58114
}

test/Arbiter.Mediation.Tests/MediatorTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,27 @@ public async Task SendWithBehaviorPipelineReflection()
107107
await Assert.That(logger.Messages[4]).Contains("Outer after");
108108
}
109109

110+
[Test]
111+
public async Task SendWithInterfaceRequestType()
112+
{
113+
var logger = new Logger();
114+
115+
var services = new ServiceCollection();
116+
services.AddMediator();
117+
118+
services.TryAddSingleton(logger);
119+
services.TryAddTransient<IRequestHandler<Ping, Pong>, PingHandler>();
120+
121+
var provider = services.BuildServiceProvider();
122+
var mediator = provider.GetRequiredService<IMediator>();
123+
124+
IRequest<Pong> request = new Ping { Message = "Ping" };
125+
var response = await mediator.Send(request);
126+
127+
await Assert.That(response).IsNotNull();
128+
await Assert.That(response!.Message).IsEqualTo("Ping Pong");
129+
}
130+
110131
[Test]
111132
public async Task SendWithBehaviorPipelineObjectType()
112133
{

0 commit comments

Comments
 (0)