.NET 6.0 の Windowsフォームアプリから、MagicOnion NuGetパッケージを使い、gRPC通信処理を行うサンプルを作成しました。
前回作成したサンプルに、データクラスを使うパターンを追加実装しました。
MagicOnionでデータクラスを通信する際は、MessagePackを使ったシリアライズ/デシリアライズが必須になります。
ソースコードはGitHubで公開しています。
.Net Frameworkで実装されたクライアントサーバシステムを .NET 6.0へ移行するにあたり、MagicOnion Nugetパッケージを使用すると、gRPC通信を使ったクライアントサーバシステムへ簡単に移行できます。
.Net Framework+WCF で実装されたクライアントサーバシステムを、.NET 6.0+WCF Core へ移行し古いHTTTPプロトコルで通信し続けるよりは、HTTP/2のgRPC通信へ移行することが推奨されています。
WCF 開発者に gRPC を推奨する理由 – WCF 開発者向け gRPC | Microsoft Learn
MagicOnionを使うと簡単にクライアント・サーバシステムでgRPC通信を実現できます。
.NETでクライアント・サーバシステムを開発する場合、通信処理はMagicOnionでgRPC通信を実装するのが主流になりそうです。
GitHubのMagicOnion プロジェクトサイトには、MagicOnion開発リポジトリ、インストール手順などがありわかり易いです。
Cysharp/MagicOnion: Unified Realtime/API framework for .NET Platform and Unity (github.com)
MagicOnion 通信インターフェース定義
ソースコード構成
最初に、サーバ側とクライアント側で使用する共通ライブラリとして、通信処理のインターフェースとデータモデルの定義を作成します。
今回使った Visual Studio プロジェクト テンプレートは、Visual Studio 2022 + .NET 6.0 + クラスライブラリです。
テンプレートに対して、下記のインターフェース定義とデータモデル定義を加えました。
ソースコード変更内容を解説
/ClassLibrary1.csproj
MagicOnion NuGetパッケージをインストール。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="MagicOnion" Version="5.1.2" /> </ItemGroup> </Project> |
/Model/ModelClassA.cs
通信する際のデータクラスを作成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using MessagePack; namespace ClassLibrary1.Model { [MessagePackObject] public class ModelClassA { [Key("IntA")] public int IntA { get; set; } [Key("StringA")] public string StringA { get; set; } } } |
/Interface/IMyFirstService.cs
通信する際のインターフェースを作成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using MagicOnion; using ClassLibrary1.Model; namespace ClassLibrary1.Interface { // Defines .NET interface as a Server/Client IDL. // The interface is shared between server and client. public interface IMyFirstService : IService<IMyFirstService> { // The return type must be `UnaryResult<T>` or `UnaryResult`. UnaryResult<int> SumAsync(int x, int y); UnaryResult<ModelClassA> Sum2Async(ModelClassA modelClassA1, ModelClassA modelClassA2); } } |
MagicOnion サーバ側
ソースコード構成
今回使った Visual Studio プロジェクト テンプレートは、Visual Studio 2022 + .NET 6.0 + ASP.NET Core(空)アプリです。
テンプレートに対して、下記の処理を加えました。
ソースコード変更内容を解説
/WebApplication1.sln
・MagicOnion通信インターフェース定義で作成したクラスライブラリをソリューションに追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.33414.496 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication1", "WebApplication1\WebApplication1.csproj", "{8579F926-C7C2-44E0-B364-750F33BD5935}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClassLibrary1", "..\ClassLibrary1\ClassLibrary1\ClassLibrary1.csproj", "{74152DB1-DCF4-401F-8946-D877CBEF580D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {8579F926-C7C2-44E0-B364-750F33BD5935}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8579F926-C7C2-44E0-B364-750F33BD5935}.Debug|Any CPU.Build.0 = Debug|Any CPU {8579F926-C7C2-44E0-B364-750F33BD5935}.Release|Any CPU.ActiveCfg = Release|Any CPU {8579F926-C7C2-44E0-B364-750F33BD5935}.Release|Any CPU.Build.0 = Release|Any CPU {74152DB1-DCF4-401F-8946-D877CBEF580D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {74152DB1-DCF4-401F-8946-D877CBEF580D}.Debug|Any CPU.Build.0 = Debug|Any CPU {74152DB1-DCF4-401F-8946-D877CBEF580D}.Release|Any CPU.ActiveCfg = Release|Any CPU {74152DB1-DCF4-401F-8946-D877CBEF580D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {005A561F-B239-4763-ABF6-56EE58856632} EndGlobalSection EndGlobal |
/WebApplication1/WebApplication1.csproj
Grpc.AspNetCore/MagicOnion.Server NuGetパッケージをインストール。
/WebApplication1/Program.cs
MagicOnion.Serverのサービス開始処理を追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using MagicOnion; using MagicOnion.Server; var builder = WebApplication.CreateBuilder(args); builder.Services.AddGrpc(); builder.Services.AddMagicOnion(); var app = builder.Build(); app.MapMagicOnionService(); app.MapGet("/", () => "Hello World!"); app.Run(); |
/WebApplication1/Services/MyFirstService.cs
interfaceに対応するサーバ側の処理を作成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
using MagicOnion.Server; using MagicOnion; using ClassLibrary1.Interface; using ClassLibrary1.Model; namespace WebApplication1.Services { // Implements RPC service in the server project. // The implementation class must inherit `ServiceBase<IMyFirstService>` and `IMyFirstService` public class MyFirstService : ServiceBase<IMyFirstService>, IMyFirstService { // `UnaryResult<T>` allows the method to be treated as `async` method. public async UnaryResult<int> SumAsync(int x, int y) { Console.WriteLine($"Received:{x}, {y}"); return x + y; } public async UnaryResult<ModelClassA> Sum2Async(ModelClassA modelClassA1, ModelClassA modelClassA2) { var ModelClassA = new ModelClassA() { IntA = modelClassA1.IntA + modelClassA2.IntA, StringA = modelClassA1.StringA + modelClassA2.StringA, }; return ModelClassA; } } } |
MagicOnion クライアント側
ソースコード構成
今回使った Visual Studio プロジェクト テンプレートは、Visual Studio 2022 + .NET 6.0 + Windowsフォームアプリです。
テンプレートに対して、下記の処理を加えました。
ソースコード変更内容を解説
/WinFormsApp1.sln
MagicOnion通信インターフェース定義で作成したクラスライブラリをソリューションに追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.33414.496 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinFormsApp1", "WinFormsApp1\WinFormsApp1.csproj", "{570826E4-29AD-4D28-8325-26FD152AAFA2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClassLibrary1", "..\ClassLibrary1\ClassLibrary1\ClassLibrary1.csproj", "{58F003E2-3F75-4AB7-B63C-B44BB0E3A089}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {570826E4-29AD-4D28-8325-26FD152AAFA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {570826E4-29AD-4D28-8325-26FD152AAFA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {570826E4-29AD-4D28-8325-26FD152AAFA2}.Release|Any CPU.ActiveCfg = Release|Any CPU {570826E4-29AD-4D28-8325-26FD152AAFA2}.Release|Any CPU.Build.0 = Release|Any CPU {58F003E2-3F75-4AB7-B63C-B44BB0E3A089}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {58F003E2-3F75-4AB7-B63C-B44BB0E3A089}.Debug|Any CPU.Build.0 = Debug|Any CPU {58F003E2-3F75-4AB7-B63C-B44BB0E3A089}.Release|Any CPU.ActiveCfg = Release|Any CPU {58F003E2-3F75-4AB7-B63C-B44BB0E3A089}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BDE73763-EB41-46C8-998F-760AA69620C4} EndGlobalSection EndGlobal |
/WinFormsApp1/WinFormsApp1.csproj
・MagicOnion.Client NuGetパッケージをインストール。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net6.0-windows</TargetFramework> <Nullable>enable</Nullable> <UseWindowsForms>true</UseWindowsForms> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <Compile Remove="Model\**" /> <Compile Remove="Shared\**" /> <EmbeddedResource Remove="Model\**" /> <EmbeddedResource Remove="Shared\**" /> <None Remove="Model\**" /> <None Remove="Shared\**" /> </ItemGroup> <ItemGroup> <PackageReference Include="MagicOnion.Client" Version="5.1.2" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\ClassLibrary1\ClassLibrary1\ClassLibrary1.csproj" /> </ItemGroup> </Project> |
/WinFormsApp1/Form1.Designer.cs/WinFormsApp1/Form1.resx
動作を確認する為に最低限の画面を作成。
/WinFormsApp1/Form1.cs
ボタンのクリックイベントに、interfaceを通しサーバ側の SumAsync()/Sum2Asyn()メソッドを呼び出す処理を作成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
using ClassLibrary1.Interface; using ClassLibrary1.Model; using Grpc.Net.Client; using MagicOnion.Client; namespace WinFormsApp1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private async void button1_Click(object sender, EventArgs e) { // Connect to the server using gRPC channel. var channel = GrpcChannel.ForAddress("https://localhost:7061"); // Create a proxy to call the server transparently. var client = MagicOnionClient.Create<IMyFirstService>(channel); // Call the server-side method using the proxy. var result = (await client.SumAsync(123, 456)); textBox1.Text = result.ToString(); var modelClassA1 = new ModelClassA() { IntA = 1, StringA = "aa" }; var modelClassA2 = new ModelClassA() { IntA = 2, StringA = "bb" }; var resultModelClassA = (await client.Sum2Async(modelClassA1, modelClassA2)); textBox1.Text = resultModelClassA.IntA.ToString() + " " + resultModelClassA.StringA; } } } |
補足
通信するデータクラスにMessagePackObject/Key属性を付与していないと、MagicOnionの通信実行時に、「MessagePackSerializationException: Failed to serialize」エラーが発生します。
通信する必要のないプロパティは [IgnoreMember]属性を加えると解消します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Grpc.Core.RpcException: 'Status(StatusCode="Internal", Detail="Error starting gRPC call. MessagePackSerializationException: Failed to serialize WebApplication1.Model.ModelClassA value. FormatterNotRegisteredException: WebApplication1.Model.ModelClassA is not registered in resolver: MessagePack.Resolvers.StandardResolver", DebugException="MessagePack.MessagePackSerializationException: Failed to serialize WebApplication1.Model.ModelClassA value. ---> MessagePack.FormatterNotRegisteredException: WebApplication1.Model.ModelClassA is not registered in resolver: MessagePack.Resolvers.StandardResolver at MessagePack.FormatterResolverExtensions.Throw(Type t, IFormatterResolver resolver) at MessagePack.MessagePackSerializer.Serialize[T](MessagePackWriter& writer, T value, MessagePackSerializerOptions options) --- End of inner exception stack trace --- at MessagePack.MessagePackSerializer.Serialize[T](MessagePackWriter& writer, T value, MessagePackSerializerOptions options) at MessagePack.MessagePackSerializer.Serialize[T](IBufferWriter`1 writer, T value, MessagePackSerializerOptions options, CancellationToken cancellationToken) at MagicOnion.Serialization.MessagePackMagicOnionSerializerProvider.MessagePackMagicOnionSerializer.Serialize[T](IBufferWriter`1 writer, T& value) at MagicOnion.GrpcMethodHelper.<>c__DisplayClass8_0`1.<CreateMarshaller>b__0(T obj, SerializationContext ctx) at Grpc.Net.Client.StreamExtensions.WriteMessageAsync[TMessage](Stream stream, GrpcCall call, TMessage message, Action`2 serializer, CallOptions callOptions) at Grpc.Net.Client.Internal.PushUnaryContent`2.WriteMessageCore(ValueTask writeMessageTask) at System.Net.Http.Http2Connection.Http2Stream.SendRequestBodyAsync(CancellationToken cancellationToken) at System.Net.Http.Http2Connection.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at Grpc.Net.Client.Internal.GrpcCall`2.RunCall(HttpRequestMessage request, Nullable`1 timeout)")' |
[Key][IgnoreMember]属性を設定していても、[Key(〇〇〇)]属性の〇〇〇パラメータが重複している場合、「Exception was thrown by handler.」例外が発生します。
1 2 3 |
Grpc.Core.RpcException: 'Status(StatusCode="Unknown", Detail="Exception was thrown by handler.")' |
コメント