ほとんどのドキュメンテーションプラットフォームは、航空会社が「レッグルーム」について話すように、「拡張性」について話します。私たちは、ラセピのアーキテクチャが予測不可能になることなく、純粋に拡張可能であることを望んだので、3つの連動したシステムを構築しました:能力のためのプラグイン、制御のためのアクションガード、そして決定論的実行のためのパイプラインです。
この投稿では、それぞれが実際のコードベースでどのように機能するかを説明する。
プラグインシステム: モジュール設計
Rasepiの全てのプラグインはIPluginModuleを実装しています - そのプラグインが何であり、どのようなサービスを必要とし、どのようなルートを公開するかを宣言する単一のインターフェイスです:
CODEBLOCK_0__
CODEBLOCK_24__は純粋なデータです。CODEBLOCK_24__は純粋なデータで、何も実行せずにプラグインを記述します:
コードブロック_1__
CODEBLOCK_25__に注目してください。この辞書は、フロントエンドの拡張ポイントをコンポーネント名にマッピングします。これにより、Vueフロントエンドは、各プラグインがどのUIコンポーネント(ツールバーのボタン、サイドバーのパネル、設定ページ)に寄与するかを知ることができます。
登録はプラグインごとに1行です。
起動時に、流暢なAPIを通じてプラグインを登録します:
codeblock_2__
それぞれの呼び出しはモジュールをインスタンス化し、レジストリに保存し、RegisterServices()を呼び出して依存関係を配線する。アプリがビルドされると、1行ですべてのプラグインのルートがマッピングされる:
CODEBLOCK_3__
プラグインは、/api/plugins/{pluginId}/でスコープされたルートグループを取得し、認可が自動的に適用されます。
実際の例: Workflowプラグイン
実際のプラグインがどのようなものかを示します - Workflow & Approvalsモジュールです:
コードブロック_4__
コア・プラットフォームはWorkflowServiceやWorkflowPublishGuardを直接参照することはありません。DIコンテナを通してそれらを発見する。コアアプリはプラグインのコードに触れない。
アクションガード:コントロールレイヤー
プラグインはケイパビリティを追加します。アクションガードは、そのケイパビリティやコアのアクションが実行されるかどうかを決定します。アクションガードは同期バリデータであり、実行前に操作をインターセプトします。
アクションガードの評価フロー](/ja/blog/img/action-guard-flow.svg)
インターフェイスは意図的に最小化されている:
コードブロック_5__
CODEBLOCK_30__がnullの場合、ガードは全てのアクションに対して実行される。これが"Entry.Publish"のように設定されると、その特定のアクションだけをインターセプトする。
コンテキストと結果の契約
すべてのガードは、アクション名、テナント、ユーザー、エンティティ、プロパティバッグを含む型付きコンテキストを受け取ります:
codeblock_6__
そして、すべてのガードは予測可能な結果を返します - allow、deny、またはallow-with-modificationsです:
codeblock_7。
CODEBLOCK_33__フィールドは重要です。ガードはアクションを承認しても、コンテンツの一部を書き換えることができます(例えば、公開前に秘密を再編集するなど)。
正規のアクション名
ガードが何をターゲットにできるかについて曖昧さがないように、すべてのインターセプト可能なアクションを文字列定数として定義します:
codeblock_8__
実際の例: 承認のない公開をブロックする
Workflow プラグインは Entry.Publish を阻止するガードを登録します:
CODEBLOCK_9__
コア・プラットフォームは承認ワークフローについて何も知らない。パイプラインを通してEntry.Publishを呼び出すだけで、ワークフローが完了していなければガードはそれをブロックします。
アクションパイプライン: すべてが収束する場所
CODEBLOCK_36__は、すべてのガードされた操作のための単一の実行パスです。CODEBLOCK_36__はどのガードが適用されるかを解決し、それらを評価し、アクションをブロックするか実行します。
CODEBLOCK_10__
CODEBLOCK_37__メソッドは重い仕事をします:
コードブロック_11__
ここで3つの重要な設計上の決定がある:
1.テナントごとの解決 - TenantPluginResolver は、各テナントがどのプラグインをインストールして有効にしているかをチェックします。無効化されたプラグインのガードは決して実行されません。
2.All-must-pass - ガードが拒否された場合、そのアクションはブロックされます。これは意図的なセキュリティスタンスです。
3.**もしガードが例外をスローした場合、それはログに記録され、Allow()として扱われます。これは壊れたプラグインがプラットフォーム全体をロックするのを防ぎます。
テナントごとのプラグイン解決
リゾルバはTenantPluginInstallationsテーブル(EFグローバルクエリフィルタによって現在のテナントに自動的にスコープされる)にクエリします:
CODEBLOCK_12__テーブル
イベント駆動型の副作用
アクションは同期。副作用は同期ではありません。アクションが完了すると、サービスはドメインイベントを発行します:
コードブロック_13__
イベントはメモリ内のチャネルにエンキューされ、バックグラウンドの EventConsumerWorker によって処理される。ワーカーは複数のシステムにイベントをルーティングします:
- アクティビティトラッキング** - 誰が、いつ、何をしたかを記録します。
- Translation billing - プロバイダごとのコストを追跡します。
- Plugin event handlers - どのプラグインでもドメインイベントをサブスクライブできる。
プラグインイベントハンドラはIPluginEventHandlerを実装しています:
コードブロック_14__
Worker は、そのテナントでプラグインが有効になっているハンドラのみを呼び出します。つまり、プラグイン A の副作用が、プラグイン B しかインストールされていないテナントに漏れることはありません。
ブロックレベルの翻訳エンジン
このアーキテクチャーが最も目に見えて効果を発揮するのはここです。
ブロックレベル翻訳:変更されたブロックだけが再翻訳される](/ja/blog/img/block-translation.svg)
従来のプラットフォームでは、文書全体を翻訳していました。私たちは、段落、見出し、リスト項目など、個々のブロックを翻訳します。ユーザーが50ブロックの文書の1つの段落を編集すると、その段落だけが再翻訳を必要とします。これが94%のコスト削減の源泉です。
TipTap JSONからのブロックの作成方法
ユーザーがドキュメントを保存すると、TipTapエディタは次のようなJSONを送信します:
コードブロック_15__
CODEBLOCK_43__はこのJSONを解析し、個々のEntryBlockレコードを作成します:
コードブロック_16__
古いレコード検出のためのSHA256ハッシュ
コンテンツ・ハッシュは古さ検出の中核である。ブロックの内容(blockIdやdeletedのようなメタデータ属性を取り除いた後)をSHA256を使ってハッシュする:
コードブロック_17__
ソースブロックが変更されると、そのハッシュも変更されます。そして、システムはすべての翻訳ブロックのSourceContentHashと現在のソース・ハッシュを比較します - 不一致はStaleとマークされます:
CODEBLOCK_48__: ```csharp
public async Task MarkTranslationsAsStaleAsync(List
foreach (var translation in affected) { translation.Status = TranslationStatus.Stale; translation.UpdatedAt = DateTime.UtcNow; }
await _context.SaveChangesAsync(); }
### 構造適応
翻訳者は、言語間でブロックタイプを変えることができる。英語の箇条書きリストがドイツ語の番号付きリストになるかもしれません。システムはこれを追跡します:
コードブロック_19__
### プラグインとしての翻訳プロバイダー
外部の翻訳サービス(DeepL、Google Translateなど)は、`ITranslationProviderPlugin`を通してプラグインします:
コードブロック
バッチメソッドは、コンテンツへのブロックIDの辞書を受信し、それらをすべて翻訳し、課金文字数で翻訳を返します。ドキュメント全体ではなく、古くなったブロックだけを送信するため、コストは最小限に抑えられます。
## テナントの分離:見えないセーフティネット
上記のシステムはすべて厳格なテナント分離の中で実行されます。
CODEBLOCK_50__はリクエストごとにJWTからテナントを解決し、メンバーシップを検証します:
コードブロック_21__
Entity Frameworkのグローバルクエリフィルタは、開発者がテナントによるフィルタリングを忘れても、データベースレイヤーが自動的に行うことを保証します:
コードブロック_22__
結果はCODEBLOCK_51__は常に現在のテナントのハブだけを返します。データ・リークには、クエリー・フィルターを積極的に回避する必要がある。
## 全体像
ユーザーがエントリーの "Publish "をクリックすると、次のようなことが起こります:
1.**認証がJWTを検証し、`TenantContextMiddleware`が解決してテナントを検証する。
2.**コントローラーがパイプラインを呼び出す** - `IActionPipeline.ExecuteAsync("Entry.Publish", context, action)` 2.
3.**パイプラインはガードを解決する** - テナントが有効にしているプラグインを照会し、該当するガードを選択する。
4.**ワークフローガードは承認をチェックし、リテンションガードはポリシーをチェックし、ルールガードはコンテンツを検証する。
5.**すべて合格?アクションが実行されます*** - エントリが公開されます
6.**イベントが発生する** - `Entry.Published`イベントがエンキューされる。
7.**バックグラウンドワーカーの処理** - アクティビティがログに記録され、翻訳請求が更新され、プラグインのイベントハンドラが呼び出されます。
8.**ブロック翻訳のチェック*** - 古いブロックは再翻訳のために識別されます。
各レイヤーはそれぞれの仕事をします。どのレイヤーも他のレイヤーには到達しません。それがアーキテクチャだ。
> 私たちがこれを作ったのは、拡張性が流行しているからではない。各チームのワークフローに適応できないドキュメンテーション・プラットフォームは、いずれ適応できるものに取って代わられるだろう。