Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docfx/docs/extensibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,25 @@ StreamJsonRpc includes the following <xref:StreamJsonRpc.IJsonRpcMessageFormatte
each message using the very fast and compact binary [MessagePack format][MessagePackFormat].
This formatter is NativeAOT ready.
Any RPC method parameters and return types that require custom serialization may provide it
with a `MessagePackConverter<T>`-derived class.
with a <xref:Nerdbank.MessagePack.MessagePackConverter`1>-derived class.
All custom converters can be added to the serializer at <xref:StreamJsonRpc.NerdbankMessagePackFormatter.UserDataSerializer>.
All RPC method parameter or return types must have type shapes generated for them via [a witness class](https://aarnott.github.io/Nerdbank.MessagePack/docs/type-shapes.html).

This formatter is not fully wire format compatible with <xref:StreamJsonRpc.MessagePackFormatter>,
so matching formatters on both sides of an RPC connection is recommended.

Create the formatter with the required <xref:StreamJsonRpc.NerdbankMessagePackFormatter.TypeShapeProvider>
and optionally with serializer customizations.
When customizing the serializer, always base the new serializer on the default one defined by <xref:StreamJsonRpc.NerdbankMessagePackFormatter.DefaultSerializer> as shown below:
Comment thread
AArnott marked this conversation as resolved.

[!code-csharp[](../../samples/Extensibility.cs#CreateNBMsgPackFormatter)]

In the above sample, the type shape provider comes from a [witness class](https://aarnott.github.io/Nerdbank.MessagePack/docs/type-shapes.html#witness-classes), which you can trivially define like this:

[!code-csharp[](../../samples/Extensibility.cs#PolyTypeWitness)]

Learn more about [witness classes](https://aarnott.github.io/Nerdbank.MessagePack/docs/type-shapes.html#witness-classes) and [customizing serialization](https://aarnott.github.io/Nerdbank.MessagePack/docs/customizing-serialization.html).

1. <xref:StreamJsonRpc.JsonMessageFormatter> - Uses Newtonsoft.Json to serialize each JSON-RPC message as actual JSON.
The text encoding is configurable via a property.
All RPC method parameters and return types must be serializable by Newtonsoft.Json.
Expand Down
6 changes: 1 addition & 5 deletions samples/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove($(MSBuildThisFile), $(MSBuildThisFileDirectory)..))" />

<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Internal.MicroBuild.NonShipping" PrivateAssets="all" />
</ItemGroup>
<Import Project="../test/$(MSBuildThisFile)" />
</Project>
4 changes: 4 additions & 0 deletions samples/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<Import Project="../test/$(MSBuildThisFile)" />
</Project>
85 changes: 85 additions & 0 deletions samples/Extensibility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.IO.Pipelines;
using Microsoft.VisualStudio.Threading;
using Nerdbank.Streams;
using Xunit;

public partial class Extensibility
{
#region RpcContract
[JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
public partial interface IWeatherService
{
Task<int> GetTemperatureAsync(string city, CancellationToken cancellationToken);
}
#endregion

#region ServiceImpl
class WeatherService : IWeatherService
{
public Task<int> GetTemperatureAsync(string city, CancellationToken cancellationToken)
{
return Task.FromResult(city switch
{
"Seattle" => 55,
"Denver" => 65,
_ => throw new ArgumentException($"Unknown city: {city}"),
});
}
}
#endregion

#region PolyTypeWitness
[GenerateShapeFor<int>]
partial class Witness;
#endregion

[Fact]
public async Task ValidateNBMsgPackSample()
{
(IDuplexPipe clientPipe, IDuplexPipe serverPipe) = FullDuplexStream.CreatePipePair();
await Task.WhenAll(
this.NBMsgPackClient(clientPipe, TestContext.Current.CancellationToken),
this.NBMsgPackServer(serverPipe, TestContext.Current.CancellationToken))
.WithCancellation(TestContext.Current.CancellationToken);
}

IJsonRpcMessageFormatter CreateNBMsgPackFormatter() =>
#region CreateNBMsgPackFormatter
new NerdbankMessagePackFormatter
{
TypeShapeProvider = Witness.GeneratedTypeShapeProvider,
UserDataSerializer = NerdbankMessagePackFormatter.DefaultSerializer with
{
PerfOverSchemaStability = true,
},
Comment thread
AArnott marked this conversation as resolved.
};
#endregion

async Task NBMsgPackClient(IDuplexPipe pipe, CancellationToken cancellationToken)
{
#region NBMsgPackClient
IWeatherService proxy = JsonRpc.Attach<IWeatherService>(
new LengthHeaderMessageHandler(
pipe,
this.CreateNBMsgPackFormatter()));
using (proxy as IDisposable)
{
int temperature = await proxy.GetTemperatureAsync("Denver", cancellationToken);
TestContext.Current.TestOutputHelper?.WriteLine($"Temp: {temperature}");
}
#endregion
}

async Task NBMsgPackServer(IDuplexPipe pipe, CancellationToken cancellationToken)
{
#region NBMsgPackServer
JsonRpc rpc = new(
new LengthHeaderMessageHandler(
pipe,
this.CreateNBMsgPackFormatter()));
rpc.AddLocalRpcTarget(RpcTargetMetadata.FromShape<IWeatherService>(), new WeatherService(), options: null);
rpc.StartListening();
await rpc.Completion;
#endregion
}
}
8 changes: 5 additions & 3 deletions samples/NativeAOT/NerdbankMessagePack.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Nerdbank.Streams;
using PolyType;
#pragma warning disable CS8892 // These Main methods will be ignored

using Nerdbank.Streams;

namespace NativeAOT;

Expand Down Expand Up @@ -36,7 +37,8 @@ class Server : IServer
public Task<int> AddAsync(int a, int b) => Task.FromResult(a + b);
}

// Every data type used in the RPC methods must be annotated for serialization.
// Just one attribute with any type argument is sufficient to trigger
// a source generator to expose the TypeShapeProvider with all types used in the app.
[GenerateShapeFor<int>]
private partial class Witness;
#endregion
Expand Down
2 changes: 2 additions & 0 deletions samples/NativeAOT/SystemTextJson.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#pragma warning disable CS8892 // These Main methods will be ignored

using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Nerdbank.Streams;
Expand Down
11 changes: 10 additions & 1 deletion samples/Samples.csproj
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(TargetFrameworks);net472</TargetFrameworks>
<IsPackable>false</IsPackable>
<OutputType>exe</OutputType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
<PolySharpIncludeRuntimeSupportedAttributes>true</PolySharpIncludeRuntimeSupportedAttributes>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\src\StreamJsonRpc\StreamJsonRpc.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="xunit.v3.mtp-v2" />
</ItemGroup>

<Import Project="$(RepoRootPath)src\AnalyzerUser.targets" />
</Project>
18 changes: 18 additions & 0 deletions src/StreamJsonRpc/NerdbankMessagePackFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ public partial class NerdbankMessagePackFormatter : FormatterBase, IJsonRpcMessa
/// <summary>
/// Initializes a new instance of the <see cref="NerdbankMessagePackFormatter"/> class.
/// </summary>
/// <remarks>
/// <para>
/// Creating this formatter requires setting the <see cref="TypeShapeProvider"/> property.
/// A type shape provider is best obtained from the <c>GeneratedTypeShapeProvider</c> property
/// that is source generated onto a "witness" class, which is a <see langword="partial"/> <see langword="class"/>
/// with at least one <see cref="GenerateShapeForAttribute{T}"/> attribute on it.
/// </para>
/// <para>
/// When customizing the <see cref="UserDataSerializer"/> property, always base the new value on the
/// <see cref="DefaultSerializer"/> property.
/// </para>
/// </remarks>
/// <example>
/// <code source="../../samples/Extensibility.cs" region="CreateNBMsgPackFormatter" lang="C#" />
/// <code source="../../samples/Extensibility.cs" region="PolyTypeWitness" lang="C#" />
/// </example>
/// <seealso href="https://aarnott.github.io/Nerdbank.MessagePack/docs/type-shapes.html#witness-classes">Witness classes</seealso>
/// <seealso href="https://aarnott.github.io/Nerdbank.MessagePack/docs/customizing-serialization.html">Customizing serialization</seealso>
public NerdbankMessagePackFormatter()
{
// Set up initial options for our own message types.
Expand Down
2 changes: 1 addition & 1 deletion tools/dirs.proj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.Build.Traversal">
<ItemGroup>
<ProjectReference Include="$(RepoRootPath)samples\Samples.csproj" />
<ProjectReference Include="$(RepoRootPath)samples\Samples.csproj" Publish="false" />
<ProjectReference Include="$(RepoRootPath)src\dirs.proj" />
<ProjectReference Include="$(RepoRootPath)test\dirs.proj" />
</ItemGroup>
Expand Down
Loading