Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
MagicOnionでの共通処理の挟み方
Search
pasta
June 04, 2019
Programming
4k
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
MagicOnionでの共通処理の挟み方
MagicOnion勉強会
https://connpass.com/event/127369/
pasta
June 04, 2019
Other Decks in Programming
See All in Programming
パズルゲームの作り方 / how to make puzzle games
kaityo256
PRO
2
290
Agentic Software Factoryに、すごく賢いIF文を。 / super smart IF statement into the Agentic Software Factory.
rkaga
8
2.2k
Herb in Rails 8.2: Your ERB views, now HTML-aware @ Rails World 2026, Austin, Texas
marcoroth
0
210
RAG の “R” を Swift で覗いてみる 〜「意味から探す」検索の仕組み〜
nao_randd
0
160
Jetpack Compose Mechanisms
skydoves
3
790
なぜCTOを降りてFDEを選んだのか?〜なぜプロダクト企業がFDEで顧客の現場に踏み込むのか〜
gonta
1
200
【加筆修正版】Laravel のアプリケーションをどこにデプロイするか #phpcon_ehime
akase244
0
150
パスワード再設定メールのURLについて知っておこう
uyuki234
0
150
AWS CDKのカスタムリソースでContinuum(旧Security Agent)を実装した話 | Implementing Continuum (formerly Security Agent) via AWS CDK Custom Resource
akihisaikeda
2
150
AIに “答え“を書かせるのを やめてみた
___akari11
0
120
Railsのためのパスキー実践入門 〜マルチテナントSaaSでの意思決定〜
utakaha
0
180
AmebaブログはKMPから卒業します
kmars0605
0
100
Featured
See All Featured
Site-Speed That Sticks
csswizardry
13
1.6k
Taking LLMs out of the black box: A practical guide to human-in-the-loop distillation
inesmontani
PRO
3
2.4k
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
State of Search Keynote: SEO is Dead Long Live SEO
ryanjones
0
320
Color Theory Basics | Prateek | Gurzu
gurzu
1
490
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
How to Talk to Developers About Accessibility
jct
2
570
Exploring anti-patterns in Rails
aemeredith
4
540
Designing Dashboards & Data Visualisations in Web Apps
destraynor
232
55k
SEO in 2025: How to Prepare for the Future of Search
ipullrank
3
3.9k
How to Think Like a Performance Engineer
csswizardry
28
2.9k
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
2
6.3k
Transcript
共通処理の挟み方 2019/06/04 MagicOnion勉強会 ぱすた(@p_a_sta) 1
だれ? ぱすた(@p_a_sta) 株式会社Donuts 新規開発部 音ゲーつくったり基盤フレームワークつくったり サーバサイド初心者 2
注意 今回は非ストリーミングAPIしか扱いません。 (ごめんなさい) 3
やりたいこと • サーバー/クライアント ◦ 共通リクエスト/レスポンスの送受信 ◦ 暗号化 • クライアント ◦
共通エラーハンドリング ◦ リトライ ◦ デバッグ出力 などなど…… 4
サーバーサイド 5
サーバーサイド 2通りのやりかた • MagicOnionFilter ◦ MagicOnionに用意された機能 • gRPC Interceptor ◦
gRPC本来の機能 どちらもRPCの前後に処理を挟むもの 6
MagicOnionFilter - 実装方法 MagicOnionFilterAttributeを継承する public class SampleFilterAttribute : MagicOnionFilterAttribute {
public override async ValueTask Invoke(ServiceContext context) { try { /* 前処理 */ await Next(context); /* 後処理 */ } catch { /* エラー処理 */ throw; } finally { /* on finally */ } } } 7
MagicOnionFilter - 適用方法 ①特定のクラス全体・または1つのメソッドに適用する場合 クラスまたはメソッドに属性として付与する // クラスに付けるとクラス全体に適用される [SampleFilter] public class
SampleService : ServiceBase<ISampleService>, ISampleService { // メソッドに付けるとメソッドだけに適用される [SampleFilter] public async UnaryResult<Nil> HogeAsync() { } } 8
MagicOnionFilter - 適用方法 ②全ての通信に適用する場合 MagicOnionOptions で指定する var options = new
MagicOnionOptions { GlobalFilters = new[] { new SampleFilterAttribute() }, }); var service = MagicOnionEngine.BuildServerServiceDefinition(options); 9
MagicOnionFilter - 概要図 10
gRPC Interceptor - 実装方法 Grpc.Core.Interceptors.Interceptor を継承する サーバ側・クライアント側それぞれについて、RPC 種類ごとにメ ソッドが用意されているので、必要なものだけを override
して実 装する 今回は UnaryServerHandler のみ説明 11
gRPC Interceptor - 実装方法 public class SampleInterceptor : Interceptor {
// ※MagicOnionを使っている限り TRequest, TResponse は byte[] 固定 public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, // 生リクエストデータ ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation ) { /* 前処理 */ var response = await continuation(request, context); /* 後処理 */ return response; } } 12
gRPC Interceptor - 適用方法 MagicOnionServiceDefinition.ServerServiceDefinition から Interceptor を刺した ServerServiceDefinition を生成する
var service = MagicOnionEngine.BuildServerServiceDefinition(); var serverService = service.ServerServiceDefinition.Intercept( new SampleInterceptor() ); 13
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 14
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.Items がリクエストごとに固有 のストレージになっている 15
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.RequestHeaders と context.WriteResponseHeadersAsync を使う 16
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.CallContext で gRPC Interceptor 側の context にアクセス可能 17
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 18
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.ForceSetRawUnaryResult で書き換えは可能 元の値は取得不可 19
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 20
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 21 要件に合う方を選んで使うのが重要
サンプル - 共通リクエスト/レスポンスの送受信 • 要件 ◦ メソッド本体との値のやり取り ◦ リクエストヘッダの読みとり ◦
レスポンスヘッダの書き換え MagicOnionFilter 22
public class CommonFilterAttribute : MagicOnionFilterAttribute { // ヘッダにバイナリを詰める時は Metadata.BinaryHeaderSuffix を付けないといけない
public const string Key = "base" + Metadata.BinaryHeaderSuffix; public override async ValueTask Invoke(ServiceContext context) { var header = context.CallContext.RequestHeaders.First(x => x.Key == Key); var commonRequest = LZ4MessagePackSerializer.Deserialize<CommonRequest>(header.ValueBytes); /* 共通リクエストに対する処理 */ context.Items[Key] = commonRequest; // メソッドで使うために詰めておく await Next(context); var commonResponse = new CommonResponse { /* 共通レスポンス作成 */ }; await context.CallContext.WriteResponseHeadersAsync(new Metadata { { Key, LZ4MessagePackSerializer.Serialize(commonResponse) } }); } } 23
サンプル - 暗号化 • 要件 ◦ リクエストボディの書き換え ◦ レスポンスボディの書き換え ◦
クライアントと共通化 gRPC Interceptor 24
public class CryptoInterceptor : Interceptor { public const string Key
= "暗号鍵"; public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation ) { request = Decrypt(request as byte[]) as TRequest; var response = await continuation(request, context); return Encrypt(response as byte[]) as TResponse; } private byte[] Encrypt(byte[] rawData) => 実装略; private byte[] Decrypt(byte[] encryptedData) => 実装略; } 25
クライアントサイド 26
クライアントサイド - 実装方法 • 基本的に gRPC Interceptor 一択 • MagicOnionClient.Create
に Interceptor を挟んだ CallInvoker を渡す var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure); var invoker = channel.Intercept(new HogeInterceptor()); var client = MagicOnionClient.Create<IHogeService>(invoker); 27
クライアントサイド - 実装方法 28 public class SampleInterceptor : Interceptor {
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation ) { /* 前処理 */ var call = continuation(request, context); return call; } }
クライアントサイド - 実装方法 29 public class SampleInterceptor : Interceptor {
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation ) { /* 前処理 */ var call = continuation(request, context); return call; } } 戻り値が Task(or task-like) じゃない →async/awaitが使えない →後処理どうするの?
クライアントサイド - 実装方法 30 var call = continuation(request, context); return
new AsyncUnaryCall<TResponse>( call.ResponseAsync.ContinueWith(res => { /* 後処理 */ return res.Result; }), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose ); • 新しい AsyncUnaryCall を作る
クライアントサイド - 実装方法 31 var call = continuation(request, context); return
new AsyncUnaryCall<TResponse>( call.ResponseAsync.ContinueWith(async res => { /* 後処理 */ return res.Result; }).Unwrap(), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose ); • 後処理で await したい場合
クライアントサイド - 実装方法 32 • 前処理で await したい場合 ◦ 不明……
エラーハンドリング 33
エラーハンドリング - 落とし穴 • 例外が発生した場合 ◦ gRPC 的には RpcException が発生するはず
◦ 実際は AggregateException が発生 ▪ Task 内部で発生した例外は AggregateException に 集約される ▪ 元の例外は .InnerExceptions で取得 34
エラーハンドリング - 落とし穴 • 例外が発生した場合 ◦ Interceptor を重ねるとその数だけ AggregateException でラップされる
▪ めちゃくちゃ取り出しづらい ▪ 本来は機能単位で Interceptor を分けるべきだが、例 外が扱いづらくなるため1クラスにまとめたほうが良い かもしれない • 結局サーバーと共通化できない 35
エラーハンドリング - リトライ • 一定の例外だったらリトライしたい • Interceptor でリトライをする方法はなさそう(※独自調べ) 拡張メソッドを作って、必ずそこ経由で呼ぶようにするしかなさそう 36
エラーハンドリング - リトライ 37 // InvokeWithErrorAsync 経由で呼ぶ MagicOnionClient.Create<IHogeService>().InvokeWithErrorAsync(x => x.HogeAsync());
// 通常はこう // MagicOnionClient.Create<IHogeService>().HogeAsync(); • イメージ (めんどくさい……)
リトライ - 実装方法 38 // IService からメソッドチェーンしたいので型引数が2つになる public static async
UniTask<T> InvokeWithErrorAsync<T, X>(this X self, Func<X, UnaryResult<T>> connector) where X : IService<X> { while (true) { try { return await connector(self); } // AggregateException から RpcException を抜き取る catch (AggregateException e) when (e.InnerException is RpcException rpc) { // 次のスライドで... } } }
リトライ - 実装方法 39 catch (AggregateException e) when (e.InnerException is
RpcException rpc) { // ここの条件は要件次第 switch (rpc.StatusCode) { case StatusCode.Unavailable: if (await リトライかタイトルに戻るかを聞く() == "リトライ") continue; SceneManager.LoadSceneAsync("Title"); // OperationCanceledException を投げて処理を打ち切ったことを通知 throw new OperationCanceledException("タイトルに戻りました", rpc); } // その他の例外はそのまま投げる throw; }
まとめ 40
まとめ • サーバー ◦ MagicOnionFilter と gRPC Interceptor のうち 要件に合う方を選んで使う
• クライアント ◦ gRPC Interceptor を使う ▪ 複数挟むと例外が扱いづらくなるので1クラスで ◦ gRPC Interceptor でできないことは泥臭く実装 41