Skip to content

Commit

Permalink
Merge pull request #241 from max-ieremenko/examples/MessagePackMarsha…
Browse files Browse the repository at this point in the history
…ller.AOT

MessagePackMarshaller.AOT example
  • Loading branch information
max-ieremenko authored Sep 28, 2024
2 parents 56c6f8f + 5c7fcc6 commit e935fce
Show file tree
Hide file tree
Showing 31 changed files with 1,144 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Examples/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.2" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />

<PackageVersion Include="MessagePack" Version="2.5.171" />
<PackageVersion Include="MessagePack.MSBuild.Tasks" Version="2.5.171" />

<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
</ItemGroup>
</Project>
28 changes: 28 additions & 0 deletions Examples/MessagePackMarshaller.AOT/Client/Client.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>

<InvariantGlobalization>true</InvariantGlobalization>
<PublishAot>true</PublishAot>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ServiceModel.Grpc.Client.DependencyInjection" />
<PackageReference Include="ServiceModel.Grpc.DesignTime" />
<PackageReference Include="ServiceModel.Grpc.MessagePackMarshaller" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Contract\Contract.csproj" />
</ItemGroup>

<ItemGroup>
<!-- Trimming hint: DataContractMarshallerFactory can be removed -->
<RuntimeHostConfigurationOption Include="ServiceModel.Grpc.DisableDataContractMarshallerFactory" Value="true" Trim="true" />
</ItemGroup>

</Project>
68 changes: 68 additions & 0 deletions Examples/MessagePackMarshaller.AOT/Client/ClientTestFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Contract;
using System.Threading.Tasks;
using System.Threading;
using Client.Tools;

namespace Client;

internal sealed class ClientTestFixture
{
private readonly ICalculator _calculator;

public ClientTestFixture(ICalculator calculator)
{
_calculator = calculator;
}

public async Task RunAsync(CancellationToken cancellationToken = default)
{
await TestCreateRectangleAsync(cancellationToken);
await TestCreateInvalidRectangleAsync(cancellationToken);
await TestShiftAsync(cancellationToken);
await TestGetVerticesAsync(cancellationToken);
await TestGetNumbersAsync(cancellationToken);
}

private async Task TestCreateRectangleAsync(CancellationToken cancellationToken)
{
var actual = await _calculator.CreateRectangleAsync((0, 2), (2, 2), (2, 0), (0, 0), cancellationToken);

actual.ShouldBe(new Rectangle((0, 2), 2, 2), RectangleComparer.Default);
}

private async Task TestCreateInvalidRectangleAsync(CancellationToken cancellationToken)
{
var ex = await Should.ThrowAsync<InvalidRectangleException>(
() => _calculator.CreateRectangleAsync((0, 2), (2, 1), (2, 0), (0, 0), cancellationToken));

ex.Message.ShouldContain("invalid rectangle");
ex.Points.ShouldBe([(0, 2), (2, 1), (2, 0), (0, 0)], PointComparer.Default);
}

private async Task TestShiftAsync(CancellationToken cancellationToken)
{
var points = AsyncEnumerable.AsAsync<Point>((0, 2), (2, 2));

var actual = await _calculator.ShiftAsync(points, 1, 2, cancellationToken);

actual.ShouldBe([(1, 4), (3, 4)], PointComparer.Default);
}

private async Task TestGetVerticesAsync(CancellationToken cancellationToken)
{
var actual = await _calculator.GetVerticesAsync(new Rectangle((0, 2), 2, 2), cancellationToken);

actual.Count.ShouldBe(4);
var points = await actual.Points.ToArrayAsync(cancellationToken);
points.ShouldBe([(0, 2), (2, 2), (2, 0), (0, 0)], PointComparer.Default);
}

private async Task TestGetNumbersAsync(CancellationToken cancellationToken)
{
var points = AsyncEnumerable.AsAsync<Point>((0, 2), (2, 2));

var actual = await _calculator.GetNumbersAsync(points, cancellationToken).ToArrayAsync(cancellationToken);

actual.ShouldBe([0, 2, 2, 2], NumberComparer.Default);
}
}
51 changes: 51 additions & 0 deletions Examples/MessagePackMarshaller.AOT/Client/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Threading.Tasks;
using Client.Services;
using Contract;
using Grpc.Net.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ServiceModel.Grpc.Client.DependencyInjection;
using ServiceModel.Grpc.Configuration;
using ServiceModel.Grpc.Interceptors;

namespace Client;

public static class Program
{
public static async Task Main()
{
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddSimpleConsole());
services.AddTransient<ClientTestFixture>();

AddApplicationServices(services, new Uri("http://localhost:5000"));

await using var provider = services.BuildServiceProvider();

await provider.GetRequiredService<ClientTestFixture>().RunAsync();
}

private static void AddApplicationServices(IServiceCollection services, Uri serverAddress)
{
services.AddTransient<LoggingClientFilter>();

services
.AddServiceModelGrpcClientFactory((options, provider) =>
{
// set MessagePackMarshaller with generated formatters as default Marshaller
options.MarshallerFactory = new MessagePackMarshallerFactory(MessagePackSerializerHelper.CreateApplicationOptions());
// Filters: log gRPC calls
options.Filters.Add(1, provider.GetRequiredService<LoggingClientFilter>());
// Error handling: activate ServerErrorHandler
options.ErrorHandler = new ClientErrorHandlerCollection(new ClientErrorHandler());
// Error handling: AOT compatible marshalling of InvalidRectangleError
options.ErrorDetailDeserializer = new ClientFaultDetailDeserializer();
})
.ConfigureDefaultChannel(ChannelProviderFactory.Singleton(GrpcChannel.ForAddress(serverAddress)))
.AddAllGrpcServices();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Contract;
using ServiceModel.Grpc.Interceptors;

namespace Client.Services;

/// <summary>
/// Error handling: raise <see cref="InvalidRectangleException"/> with details from <see cref="InvalidRectangleError"/>
/// </summary>
internal sealed class ClientErrorHandler : IClientErrorHandler
{
public void ThrowOrIgnore(ClientCallInterceptorContext context, ClientFaultDetail detail)
{
if (detail.Detail is InvalidRectangleError error)
{
throw new InvalidRectangleException(error.Message, detail.OriginalError, error.Points);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using Contract;
using ServiceModel.Grpc.Configuration;
using ServiceModel.Grpc.Interceptors;

namespace Client.Services;

/// <summary>
/// Error handling: AOT compatible deserialization of <see cref="InvalidRectangleError"/>
/// </summary>
internal sealed class ClientFaultDetailDeserializer : IClientFaultDetailDeserializer
{
public Type DeserializeDetailType(string typePayload)
{
if (typePayload == nameof(InvalidRectangleError))
{
return typeof(InvalidRectangleError);
}

throw new NotSupportedException();
}

public object DeserializeDetail(IMarshallerFactory marshallerFactory, Type detailType, byte[] detailPayload)
{
if (detailType != typeof(InvalidRectangleError))
{
throw new NotSupportedException();
}

return MarshallerExtensions.Deserialize(marshallerFactory.CreateMarshaller<InvalidRectangleError>(), detailPayload);
}
}
16 changes: 16 additions & 0 deletions Examples/MessagePackMarshaller.AOT/Client/Services/GrpcServices.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Contract;
using ServiceModel.Grpc.Client.DependencyInjection;
using ServiceModel.Grpc.DesignTime;

namespace Client.Services;

[ImportGrpcService(typeof(ICalculator), GenerateDependencyInjectionExtensions = true)]// instruct ServiceModel.Grpc.DesignTime to generate required code during the build process
[MessagePackDesignTimeExtension] // instruct ServiceModel.Grpc.MessagePackMarshaller to generate required code during the build process
internal static partial class GrpcServices
{
public static void AddAllGrpcServices(this IClientFactoryBuilder factoryBuilder)
{
// map generated ICalculator client
factoryBuilder.AddCalculatorClient();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ServiceModel.Grpc.Filters;

namespace Client.Services;

/// <summary>
/// Filters: log all gRPC calls
/// </summary>
internal sealed class LoggingClientFilter : IClientFilter
{
private readonly ILogger _logger;

public LoggingClientFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger("ClientFilter");
}

public void Invoke(IClientFilterContext context, Action next)
{
OnRequest(context);
try
{
next();
}
catch (Exception ex)
{
OnException(context, ex);
throw;
}

OnResponse(context);
}

public async ValueTask InvokeAsync(IClientFilterContext context, Func<ValueTask> next)
{
OnRequest(context);
try
{
await next();
}
catch (Exception ex)
{
OnException(context, ex);
throw;
}

OnResponse(context);
}

private void OnRequest(IClientFilterContext context) =>
_logger.LogInformation("request {signature}", GetSignature(context.ContractMethodInfo, context.Request));

private void OnResponse(IClientFilterContext context) =>
_logger.LogInformation("response {signature}", GetSignature(context.ContractMethodInfo, context.Response));

private void OnException(IClientFilterContext context, Exception ex) =>
_logger.LogError(ex, $"{context.ContractMethodInfo.DeclaringType?.Name}.{context.ContractMethodInfo.Name}");

private static string GetSignature(MethodInfo method, IEnumerable<KeyValuePair<string, object?>> parameters)
{
var message = new StringBuilder()
.Append($"{method.DeclaringType?.Name}.{method.Name}")
.Append(' ');

var comma = false;
foreach (var entry in parameters)
{
if (comma)
{
message.Append(", ");
}

comma = true;
message
.Append(entry.Key)
.Append(':')
.Append(entry.Value);
}

return message.ToString();
}
}
29 changes: 29 additions & 0 deletions Examples/MessagePackMarshaller.AOT/Client/Tools/AsyncEnumerable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Client.Tools;

internal static class AsyncEnumerable
{
public static async IAsyncEnumerable<T> AsAsync<T>(params T[] values)
{
await Task.CompletedTask;

foreach (var value in values)
{
yield return value;
}
}

public static async Task<T[]> ToArrayAsync<T>(this IAsyncEnumerable<T> enumerable, CancellationToken cancellationToken = default)
{
var result = new List<T>();
await foreach (var item in enumerable.WithCancellation(cancellationToken))
{
result.Add(item);
}

return result.ToArray();
}
}
Loading

0 comments on commit e935fce

Please sign in to comment.