20210503のC#に関する記事は6件です。

【Unity】最強のメッセージングライブラリ「MessagePipe」のメリットと使いどころを解説してみた

はじめに 最近はVRMmodの人と認識されるようになった@yoship1639です。 久々の投稿となります。 皆様、MessagePipeというライブラリを聞いたことはありますでしょうか。 多分ほとんどの方が聞いた事が無いと思います。なぜならリリースからまだ1週間しか経っていませんので…(2021/5/3現在) このMessagePipe、ポテンシャルがやばいので是非いろんな方に知っていただきたくて早めに記事にしました。 個人的にはUniRxやUniTaskレベルで使えるのではと思っています。 ※ ちなみに私が触った所感なので、本記事で述べるMessagePipeのメリットや使い方が正しいかどうかはまだ不明です MessagePipeについては既に以下の記事が投稿されておりますので、どのような機能があるかはこちらを参考にしていただければと思います。 本記事では細かい機能は述べません。 MessagePipe入門:序 MessagePipe入門:破 まずは、簡単にどんなライブラリか説明します。 MessagePipeとは MessagePipeは、C#大統一理論でお馴染みneuecc先生が開発し2021/4/26にリリースした.Net, Unity用メッセージングパイプラインライブラリです。 嚙み砕いて言うと、「C#標準のeventよりも高速な多機能イベント・分散メッセージング用ライブラリ」となります。 主な用途として、Readmeには以下の様に記載されています。 Pub/Sub CQRSのMediatorパターン PrismのEventAggregator(V-VM decoupling) etc Unityの場合、オブジェクト同士のメッセージングや通信の仲介役として使われる想定ですね。 MessagePipeがどの位パフォーマンスがあるかというと この位高速らしいです。RxのSubjectやC#標準eventよりも高速なのは目を引きます。 MessagePipeのメリット そんなMessagePipeですが、どうしてポテンシャルがあると私が思ったかというと ずばり、オブジェクト同士が互いを認識することなく最小限のインターフェースで超高速にやり取りできるからです。 この一言でこのライブラリの凄さに気づいた方は鋭いです。 Unityでは今までオブジェクト同士がお互いが参照しあったり、あるマネージャが他のマネージャのメソッドを呼んだりなど密になった状態が当たり前となっていました。これはUnityでゲームを作っている方には避けられない道であるかと思います。イベント処理だとUniRxを使う方も多いですが、UniRxでもイベントの仲介者はサポートしていないため、オブジェクトのどちらか片方がもう片方の実態を認識しメッセージを送る必要がありました。 sample.cs class Enemy { public int Hp { get; set; } } class Player { public int Hp { get; set; } public int Atk { get; set; } // Player は Enemy というクラスを知っていなくてはならない public void Attack(Enemy enemy) { // 超適当な敵への攻撃 enemy.Hp -= Atk; } } これを回避するには、インターフェースを作成しそれを仲介させることでお互いの依存度を下げる対処を行う必要がありました。これは、有効な手段ですがUnityだと煩雑になりかねないのでインターフェースを記述せずに直接メッセージングを行ったりする事も多いのではないかと思います。 sample2.cs interface IDamageable { int Hp { get; set; } } class Enemy : IDamageable { public int Hp { get; set; } } class Player { public int Hp { get; set; } public int Atk { get; set; } // Player は IDamageable に対してダメージを与える // この場合、プレイヤーはEnemyの存在を知らなくてもよいが、インターフェースを定義していなければならない public void Attack(IDamageable damageable) { damageable.Hp -= Atk; } } これ以外にもAttackManagerを実装してAttackManagerがプレイヤーをとエネミーの攻撃処理を取りまとめる事もできますが、結局AttackManager依存となるため根本的な問題は解決しません。 ですが、MessagePipeはこれらの厄介な問題を一気に解決してくれます。 オブジェクトが互いを認識しなくても、AマネージャがBマネージャを認識しなくても、仲介役のインターフェースを記述しなくても他のオブジェクトにメッセージを送ることができるのです。詳しい例は次の章でご紹介します。 互いを認識せずともメッセージを送る事ができて一体どんなメリットがあるのか、それは最大限の疎結合化・モジュール化の手段となる事です。 分野にもよりますが、Unityを使ったゲーム開発であれば、これは設計においてエンジニアが目指すべき領域です。 疎結合化やモジュール化のメリットはググるとたくさん出てきますので、これらのメリットが解らない方はぜひ調べてみてください。 ※ 因みにここで指すモジュール化はオブジェクト指向の型によるモジュール化です。手続き型のモジュール化とは違います。 では、実際にMessagePipeの使用例を見てみましょう。 MessagePipeの使用例 先ほどの例で考えます。例えば3DアクションでプレイヤーがエネミーにZキーでダメージを与えたい場合、MessagePipeを使うとどのように記述できるのかをご紹介します。 まず、どんなメッセージがあればプレイヤーがエネミーにダメージを与えることができるのかを考えます。 PlayerAttackData.cs using UnityEngine; public class PlayerAttackData { public Vector3 position; // どの位置で public float radius; // どの範囲で public int value; // どの位のダメージで } 一先ず上のデータがあれば最低限のダメージデータとしては成り立ちそうです。 次に、AttackDataをMessagePipeで使うために登録します。 MessagePipeはver1.2.0時点ではDIコンテナを使う必要がありますので、今回は「Zenject」を使います。 ZenjectInstaller.cs using MessagePipe; using UnityEngine; using Zenject; public class ZenjectInstaller : MonoInstaller { public override void InstallBindings() { // MessagePipeをZenjectContainerにバインド var option = Container.BindMessagePipe(); // PlayerAttackDataのイベントを登録 Container.BindMessageBroker<PlayerAttackData>(option); } } これで、プレイヤーからエネミーに行くダメージイベントの登録が完了しました。 次に攻撃イベント発する機能をもつプレイヤークラスを記述します。 Zキーで攻撃イベントを発します。 Player.cs using MessagePipe; using UnityEngine; using Zenject; public class Player : MonoBehaviour { // Zenjectによって自動的に攻撃イベントが注入される [Inject] IPublisher<PlayerAttackData> AttackEvent { get; set; } [SerializeField] private int hp; [SerializeField] private int atk; void Update() { if (Input.GetKeyDown(KeyCode.Z)) { Debug.Log("プレイヤーの攻撃"); // Zキーで攻撃。攻撃データを発するだけ AttackEvent.Publish(new PlayerAttackData() { position = transform.position + transform.forward, // プレイヤーの前方 radius = 1.0f, // 半径1m value = atk, // ダメージ量 }); } } } 上のコードを見ていただければ分かりますが、Playerは攻撃データを発する事しかしていません。そこにはEnemyもIDamageableもありません。 IPublisherがMessagePipeのイベント機能です。名前の通りイベントをPublishします。 ここにはZenjectから勝手にInjectされるので個別にIPublisherを代入する必要はありません。 最後に攻撃イベントを受けるエネミークラスを記述します。 Enemy.cs using System; using MessagePipe; using UnityEngine; using Zenject; public class Enemy : MonoBehaviour { // Zenjectによって自動的に攻撃イベントが注入される [Inject] ISubscriber<PlayerAttackData> OnAttacked { get; set; } [SerializeField] private int hp; private IDisposable disposable; void Awake() { var d = DisposableBag.CreateBuilder(); OnAttacked.Subscribe(attack => { if (Vector3.Distance(transform.position, attack.position) <= attack.radius) { // プレイヤーの攻撃範囲内の場合、ダメージを受ける hp -= attack.value; Debug.Log("エネミーはダメージを受けた"); if (hp <= 0) { Debug.Log("エネミーは倒れた"); Destroy(gameObject); } } }).AddTo(d); disposable = d.Build(); } void OnDestroy() { // 破棄されるタイミングでOnAttackイベントの購読をやめる disposable.Dispose(); } } どこかしらから来たプレイヤーの攻撃イベントを受け取って、攻撃を受ける範囲内の場合にダメージを受ける処理を記述しています。 Enemyクラスを見ればわかりますがそこにはPlayerの存在がありません、MessagePipeを通して来た攻撃イベントを元にダメージを受ける処理を記述しているだけです。 これで、Zキーを押すたびに前方にいるエネミーにダメージを与えられます。 ZenjectInstaller、Player、Enemyすべてのクラスでお互いの実態を認識していません。そこにはAttackDataという攻撃のデータが行き来するだけです。 これがMessagePipeの凄いところです。 Unityでの使いどころ 使用例を見ただけではMessagePipeをどのようなところで使えばいいのか分からないと思うので、簡単にご紹介します。 モジュール同士のような広いスコープでのイベント処理 まず、使うべき場所はモジュール同士のような広いスコープでのイベント処理です。 イベントといえばUniRxも柔軟にイベントを処理できるライブラリですが、どちらか片方が相手の実態を認識していなければなりません。もし、UniRxを広いスコープで利用するとどうなるかというと、全く関係のないオブジェクト同士が密に繋がってしまい疎結合化やモジュール化ができなくなります。 しかし、広いスコープではMessagePipeを使えばこれを回避できます。モジュール同士が結合度を高めてしまう事をほぼ完全に防げるのです。 関係性が薄いオブジェクト同士がイベントのやり取りをしなければならない時 次に、関係性が薄いオブジェクト同士のイベント処理です。 プレイヤーとエネミーは互いに独立していた方が良いのはもちろんですが、どうしてもそれらをつながなければならない場合があります。先ほどの攻撃処理がそうですね。この場合はMessagePipeを使った方が良いです。C#標準のeventやUniRxでも同じような処理はできますが、先ほども述べたように、どちらかがどちらかの実態を認識する必要があります。これを回避できるのはありがたいですね。 逆を言えば関連性のあるオブジェクト同士には今まで通りUniRxやeventを使った方が良いです。わざわざMessagePipeを介すだけ無駄なので。 ボトムアップ型の設計をしたUnityプロジェクト 最後に、ボトムアップ型の設計をしたUnityプロジェクトです。 これを聞いてピンとこない方も多いかと思いますが、設計にはボトムアップ型とトップダウン型の2種類の設計があります。簡単に説明すると、○○マネージャが○○オブジェクトを操作、管理する手法がトップダウン設計で、逆に個々のオブジェクトが自発的にふるまいをしてプロジェクト全体を成り立たせるのがボトムアップ型の設計です。ざっくりいうと前者は手続き指向で後者はオブジェクト指向だと思ってください。細かい話はここではしないので各自調べていただければと思います。 どちらの設計が良いかは作ろうとしているプロジェクトによりますが、ゲーム開発において私がお勧めしているのはボトムアップ型の設計です。最速の処理速度を目指すなら手続き型が最適解になりますが、機能追加等の仕様変更や柔軟性に強いプロジェクトを目指すならボトムアップ型の方が良いです。 MessagePipeはオブジェクト同士がやり取りするためのライブラリなので、上から管理しているトップダウン型のプロジェクトにはあまり向きません。なぜなら、上が下の実態を認識しておりメッセージの仲介を上が行っているので疎なメッセージングを行う必要性が薄いからです。 しかし、ボトムアップ型だと効果は絶大です。MessagePipeはオブジェクト指向が目指している関心の分離、疎結合化を解決する最適な手段になり得ます。 おわりに まだMessagePipeはリリースされたばかりで文献が全くない状態なので本記事の内容が本当に正しいかは分かりません、ただ、いち早く触ってみた私の所感は記述した通りで大方まとめたつもりです。UniRxやZenjectといった前提知識が求められるライブラリで学習には少しハードルが高いかもしれませんが、MessagePipeはUniRxやUniTaskなどと並び得るライブラリだと個人的には思っています。それだけポテンシャルを持っています。 勘違いしてはならないのが、MessagePipeを使えば疎結合化が簡単に実現できる訳では無いという事です。これはどのようなライブラリにも言えますが、使い方を間違えると逆に悪化してしまう可能性があることを念頭に置いてください。特にUniRxやUniTask等がまだうまく使えないUnity初心者の方には全くお勧めできないライブラリです。UnRx、UniTask、ZenjectやVContainerといったライブラリの使い方を知っている、メリットも分かっている方には非常にお勧めです。 MessagePipeを試してみたいけど一々準備するのが面倒な方や、どう記述できるのか学習したい方向けにGithubにMessagePipeTestというプロジェクトをあげているので、軽く見てみたい方はぜひクローンしてみてください、必要なパッケージは自動でインポートされます。できれば更新してくださいお願いしますお願いします。。。 https://github.com/yoship1639/MessagePipeTest この記事がMessagePipeを導入しようとするきっかけになっていただければ幸いです! 結合度に関する追記(21/05/04) 勘違いされないためにも一応追記しておきます。 MessagePipeを使って結合度を下げる事は保守や変更に対して強くなるので良い事ではありますが、やり過ぎは禁物です。Pub/Subだらけにするなど行き過ぎた疎結合化はプロジェクト全体像を把握する事ができなくなります。すると逆にプロジェクトが保守できなくなる可能性もあります。これでは導入した意味がなくなってしまうので、適材適所に使うようにしてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UbuntuでC#のアプリを開発する

はじめに C#のアプリを開発するための環境をUbuntu20.04.2LTSサーバ上につくります。MacからVS Codeを使用してUbuntuにリモート接続します。 Windowsからでも基本的には同じだと思います。 VS Codeの操作に慣れている方を対象としています。 用意するもの Ubuntu20.04.2LTS サーバ Mac VS Code SSHでサーバに接続 VS Codeに「Remote - SSH」拡張機能をインストールします。 UbuntuサーバにVS Codeから「Remote - SSH」で接続します。 Ubuntu20.04.2LTS サーバのセットアップ 今回は、UbuntuサーバをMacのVM上(parallels desktop)に作成しました。 クラウドでも実マシンでも要領は同じだと思います。 VS Codeのターミナルから以下のコマンドを実行します。 .Net 5.0をapt-getでインストールできるようにします。 wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb sudo dpkg -i packages-microsoft-prod.deb .Net 5.0 SDKをインストールします。 sudo apt-get update sudo apt-get install -y apt-transport-https sudo apt-get install -y dotnet-sdk-5.0 VS Code 次に、Ubuntuサーバでデバッグ等が行えるように、C#の拡張機能をインストールします。リモート接続した状態で、「C# ms-dotnettools.csharp」を表示すると、図のようにinstall in SSH: xxx.xxx.xxx.xxxと表示されます。この状態で、インストールすると拡張機能がUbuntu側にインストールされます。Mac側には必要ありません。 プロジェクトの作成 VS Codeのターミナルから以下のコマンドを実行してプロジェクト作成します。 dotnet new console --name CSharpTest ここで、フォルダを開いている場合は、一旦フォルダを閉じて、CSharpTestフォルダを開きます。 次に、メニューからTerminal->Run Build Taskを選択すると、右下にC#の拡張機能がビルドの設定を作成するかどうか聞いてくるので、Yesを選択します。 プロジェクトフォルダ(この例では、CSharpTest)に、.vscodeフォルダが作成され、その中に、ビルド(tasks.json)と実行(launch.json)の設定ファイルが作成されます。 うまくいかなかった場合、.vscodeフォルダを削除して、sshを切断してからやり直します。 また、プロジェクトのフォルダを開いた状態で行わないと、.vscodeが変な場所に作成されてしまってうまくいかないことがあります。 ビルドとデバッグ 三角マークに虫のアイコンがあるボタンを押すと Runボタンが表示されます。 なぜか、.NET Core Launch(console)が2つ表示されていますが同じものです。(バグ?) Launchが選択されていることを確認します。 Runボタンを押すとデバッグ実行されます。 ブレークポイントを設定しておくと、ブレークポイントで停止します。 まとめ サーバサイドのプログラムをMacのVS Codeから開発、デバッグできるようになりました。 VS Codeは便利で大好きなのですが、うまくいかないとなかなか解決できずにハマってしまうので、いろいろ使い方をまとめていきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# CancellationTokenSource の解放を簡単かつ確実に実装する方法はないものか

解決したいこと クライアントアプリで非同期タスクをキャンセルできるようにするとき次のようなコードを実装しますが、CancellationTokenSource に関する実装をもっと簡単にしたいと考えています。 CancellationTokenSource の解放漏れを防ぎたい。 単純なタスクのキャンセルは、CancellationTokenSource を過度に意識することなく実装できるようにしたい。 対象のタスクの多重実行を認める場合はタスクとキャンセルトークンの組み合わせをコレクションで管理するような実装が必要になるが、これを簡単に実装したい。 単純な非同期タスクのキャンセル // 実行中のタスクをキャンセルするためのキャンセルトークン private CancellationTokenSource? m_CurrentCancellation; /// <summary> /// 実行します。 /// </summary> /// <returns></returns> private async Task RunAsync() { using CancellationTokenSource cancellation = new CancellationTokenSource(); m_CurrentCancellation = cancellation; try { await ExecuteAsync(cancellation.Token).ConfigureAwait(false); } finally { m_CurrentCancellation = null; } } /// <summary> /// 実行中のタスクをキャンセルします。 /// </summary> private void Cancel() { m_CurrentCancellation?.Cancel(); } /// <summary> /// 実行対象の非同期処理 /// </summary> /// <param name="cancellation">キャンセルトークン</param> /// <returns></returns> private Task ExecuteAsync(CancellationToken cancellation = default) { return Task.Delay(1000); } キャンセル処理をカプセル化する CancellationTokenSource に関する実装をカプセル化したクラスを定義すると、前述のサンプルコードは次のように書き換えることができそうです。CancellationTokenSource の代わりに Guid を用いてキャンセルしたいタスクに対応するキャンセルトークンを指定することにしました。 TaskManagerクラス public class TaskManager { // 実行中のタスクをキャンセルするためのキャンセルトークン private readonly ConcurrentDictionary<Guid, CancellationTokenSource> m_CurrentCancellations = new ConcurrentDictionary<Guid, CancellationTokenSource>(); /// <summary> /// 指定された非同期メソッドを実行します。 /// </summary> /// <param name="taskId">タスクを一意に識別するID</param> /// <param name="function">実行する非同期メソッド</param> /// <returns></returns> private async Task RunWithCancellation(Guid taskId, Func<CancellationToken, Task> function) { using CancellationTokenSource cancellation = new CancellationTokenSource(); try { if (!m_CurrentCancellations.TryAdd(taskId, cancellation)) { throw new ArgumentException("指定されたタスクIDは既に登録されています。"); } await ExecuteAsync(cancellation.Token).ConfigureAwait(false); } finally { m_CurrentCancellations.Remove(taskId, out _); } } /// <summary> /// 指定されたタスクをキャンセルします。 /// </summary> /// <param name="taskId">タスクを一意に識別するID</param> public void Cancel(Guid taskId) { if (m_CurrentCancellations.TryGetValue(taskId, out var cancellation) { cancellation.Cancel(); } } } キャンセルトークンの代わりにタスクIDを保持しておかなくてはならない煩わしさは残りますが、CancellationTokenSource をカプセル化することができました。TaskManager クラスに IDisposable インターフェースを実装し、実行中のタスクをキャンセル&トークンを解放するようにすれば解放漏れを防ぎやすくなると思います。 TaskManagerクラスを用いたサンプルコード private readonly TaskManager m_Manager = new TaskManager(); // 実行中のタスクを表すID private Guid? m_CurrentTaskId; /// <summary> /// 実行します。 /// </summary> /// <returns></returns> private async Task RunAsync() { m_CurrentTaskId = Guid.NewGuid(); try { await m_Manager.RunWithCancellation(currentTaskId, ExecuteAsync).ConfigureAwait(false); } finally { m_CurrentTaskId = null; } } /// <summary> /// 実行中のタスクをキャンセルします。 /// </summary> private void Cancel() { if (m_CurrentTaskId.HasValue) { m_Manager.Cancel(m_CurrentTaskId.Value); } } /// <summary> /// 実行対象の非同期処理 /// </summary> /// <param name="cancellation">キャンセルトークン</param> /// <returns></returns> private Task ExecuteAsync(CancellationToken cancellation = default) { return Task.Delay(1000); } 実装したクラス リスナーによる完了通知機能を加えた TaskObserver クラスとして実装しました。 サンプルコード private readonly TaskObserver TaskObserver = new TaskObserver(); private IDisposable? TaskListenerUnregister; /// <summary> /// TaskObserver に関する初期処理 /// </summary> private void InitializeTaskObserver() { TaskListenerUnregister = TaskObserver.RegisterListener(OnStart, OnCompleted, OnFailed); } /// <summary> /// タスクが開始されるときの処理を行います。 /// </summary> /// <param name="taskId"></param> private void OnStart(TaskID taskId) { Debug.WriteLine($"Start the task. ID:{taskId}"); } /// <summary> /// タスクが完了したときの処理を行います。 /// </summary> /// <param name="taskId"></param> private void OnCompleted(TaskID taskId) { Debug.WriteLine($"The task is complete. ID:{taskId}"); } /// <summary> /// タスクが失敗したときの処理を行います。 /// </summary> /// <param name="taskId"></param> /// <param name="exception"></param> private void OnFailed(TaskID taskId, Exception? exception) { Debug.WriteLine($"The task is failed. {exception?.Message} ID:{taskId}"); } /// <summary> /// 明示的に生成されたキャンセルトークンを使用して非同期処理を実行します。 /// </summary> /// <returns></returns> private Task ExecuteActionWithCancellationToken(Guid taskId, CancellationTokenSource cancellation) { return TaskObserver.Run( taskId , ExecuteActionAsync , cancellation , disposableCancellation: false ); } /// <summary> /// 暗黙的に生成されるキャンセルトークンを使用して非同期処理を実行します。 /// </summary> /// <returns></returns> private Task ExecuteActionWithImplicitCancellationToken(Guid taskId) { return TaskObserver.RunWithCancellation( taskId , ExecuteActionAsync ); } /// <summary> /// 明示的に生成されたキャンセルトークンを使用して非同期処理を実行します。 /// </summary> /// <returns></returns> private Task<int> ExecuteFunctionWithCancellationToken(Guid taskId, CancellationTokenSource cancellation) { return TaskObserver.Run( taskId , ExecuteFunctionAsync , cancellation , disposableCancellation: false ); } /// <summary> /// 暗黙的に生成されるキャンセルトークンを使用して非同期処理を実行します。 /// </summary> /// <returns></returns> private Task<int> ExecuteFunctionWithImplicitCancellationToken(Guid taskId) { return TaskObserver.RunWithCancellation( taskId , ExecuteFunctionAsync ); } /// <summary> /// 明示的に生成されたキャンセルトークンによって制御されるタスクを監視します。 /// </summary> /// <param name="taskId"></param> /// <param name="cancellation"></param> /// <returns></returns> private Task ObserveActionWithCancellationToken(Guid taskId, CancellationTokenSource cancellation) { return TaskObserver.Observe( taskId , ExecuteActionAsync(cancellation.Token) , cancellation , disposableCancellation: false ); } /// <summary> /// 暗黙的に生成されるキャンセルトークンによって制御されるタスクを監視します。 /// </summary> /// <param name="taskId"></param> /// <param name="cancellation"></param> /// <returns></returns> private Task ObserveActionWithImplicitCancellationToken(Guid taskId) { var cancellation = new CancellationTokenSource(); return TaskObserver.Observe( taskId , ExecuteActionAsync(cancellation.Token) , cancellation , disposableCancellation: true ); } /// <summary> /// 明示的に生成されたキャンセルトークンによって制御されるタスクを監視します。 /// </summary> /// <param name="taskId"></param> /// <param name="cancellation"></param> /// <returns></returns> private Task<int> ObserveFunctionWithCancellationToken(Guid taskId, CancellationTokenSource cancellation) { return TaskObserver.Observe( taskId , ExecuteFunctionAsync(cancellation.Token) , cancellation , disposableCancellation: false ); } /// <summary> /// 暗黙的に生成されるキャンセルトークンによって制御されるタスクを監視します。 /// </summary> /// <param name="taskId"></param> /// <param name="cancellation"></param> /// <returns></returns> private Task<int> ObserveFunctionWithImplicitCancellationToken(Guid taskId) { var cancellation = new CancellationTokenSource(); return TaskObserver.Observe( taskId , ExecuteFunctionAsync(cancellation.Token) , cancellation , disposableCancellation: true ); } /// <summary> /// 指定されたタスクをキャンセルします。 /// </summary> /// <param name="taskId"></param> private void Cancel(Guid taskId) { TaskObserver.RequestCancel(taskId); } /// <summary> /// 全てのタスクをキャンセルします。 /// </summary> private void CancelAll() { TaskObserver.RequestCancelAll(); } /// <summary> /// 指定されたタスクが実行中かどうかを取得します。 /// </summary> /// <param name="taskId"></param> /// <returns></returns> private bool IsRunning(Guid taskId) { return TaskObserver.IsRunning(taskId); } /// <summary> /// 非同期処理のサンプル /// </summary> /// <param name="cancellation"></param> /// <returns></returns> private Task ExecuteActionAsync(CancellationToken cancellation = default) { return Task.Delay(1000); } /// <summary> /// 戻り値を返す非同期処理のサンプル /// </summary> /// <param name="cancellation"></param> /// <returns></returns> private Task<int> ExecuteFunctionAsync(CancellationToken cancellation = default) { return Task.Delay(1000).ContinueWith(t => 10); } まとめ CancellationTokenSource に関連する実装量を減らすことができたと思います。 Task, Task<TResult> から fluently に記述できたりなど、もっとスマートに実装できる方法があるとよいのですが。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.NET Core WebAppにおけるajax通信のメモ

概要 .NET Core WebAppにおいて、ajax通信をする際のクライアントサイド・サーバサイドのお作法を時々忘れてしまうので、 世の中には同じ方がおられるかもしれない、ということで忘備録としてまとめておきます。 クライアントサイド ・URLについて 「/Hoge/」については、C#プロジェクトの「Pages」フォルダ直下のフォルダ名を指定します。 「/Index/」は上記フォルダ直下のファイル名を指定します。 例: 「/Pages/Hoge/Index.cshtml.cs」ファイルにリクエストする場合、リクエストURLは「/Hoge/Index/」と指定します。 その後続けて「?handler=」(決り文句)を記述し、 「GetList」はサーバサイドのメソッド名を指定します。 パラメーターを追加する場合は、「&{キー}={値}」と連ねて記述できます。 POSTの場合は、typeに「POST」を指定し、 「data: JSON.stringify({JSON})」プロパティを追加するだけの違いです。 $.ajax({ type: 'GET', url: '/Hoge/Index/?handler=GetList&param1=1&param2=2', dataType: 'JSON', contentType: 'application/json; charset=utf-8', beforeSend: function (xhr) { xhr.setRequestHeader("XSRF-TOKEN", $('input:hidden[name="__RequestVerificationToken"]').val()); }, processData: true, cache: false, }).done(function (data) { // OK時の処理 }).fail(function (data) { // NG時の処理 }); サーバサイド ・メソッド名のプレフィックスに「OnGet」を書く必要があります。 ただし、クライアントサイドからリクエストする場合は、「OnGet」は省略します。 例: メソッド名が「OnGetGetList」の場合、リクエスト先URLは「GetList」となります。 ・返り値の型はIActionResultになります。 public IActionResult OnGetGetList(int param1, int param2) { try { } catch (Exception e) { } return new JsonResult(json); }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MessagePipe入門:破

前回のMessagePipe入門では、初投稿にも関わらず反響をいただけたので、味をしめた私は、続編を書くことを決定しました。 MessagePipeは布教したいですし、こうなったら主要な機能は一通り解説しておきたいですね。 今回は、前回残したEventFactoryと、RequestHandler機能を主にご紹介したいと思います。 EventFactory その名の通り、(MessagePipeによる)eventを作るものです。 通常のPub/Subでは、DIにより、型でワイヤリングされていますが、EventFactoryからイベントの個別のインスタンスを作り出すことが出来ます。 IDisposablePublisher<int> countPublisher; public ISubscriber<int> OnCount { get; } ctor(EventFactory eventFactory) { //戻り値がタプルなので、分割代入が便利。 (countPublisher, OnCount) = eventFactory.CreateEvent<int>(); //CreateEventAsyncもあります。 } 使い方は様々だと思いますが、このような状況であっても、MessagePipeに統一することが可能であるということを覚えておけば良いのではないかな~と思っています。 Request/Response これは、Mediatorパターンの実装に有効な機能なようです。 実は筆者、デザインパターンに明るくなく、Mediatorパターンについては知識が浅かったりします。また、GoFデザパタに関しては、実際に必要となる状況に当たり、実装を通して理解した概念に後から名前を付け、他のエンジニアとの会話を楽にするものだと思うぐらいがちょうど良いと思っていますので、Mediatorの説明は他の方におまかせしたいと思います。 とは言え、用途は検討がつきます。 またですが、メッセージアプリを作りたいとしましょう。 まず、MessagePipeにおけるRequest/Responce機能の実装とは、以下のようにIRequestHandler<TMessage,TResponse>を実装することです。 public class MessageHandler: IRequestHandler<MessageRequest,MessageResponce> { //コンストラクタ public MessageHandler(...) { ... //いろいろなものを使って色々やりたいとする。 //(ユーザー情報のリポジトリに接続、グループの情報を取得する等... } MessageResponce IRequestHandler.Invoke(MessageRequest req) { if(user.isNantoka) { ... //色々 var responce = ...; //色々あった。 return response; } throw new Exception("Somthing wrong has happened"); } } 次に、MessageをAPIから送るコントローラーが有るとしましょう。 (※妄想フレームワークです) public class MessageController: ControllerBase { IRequestHandler<MessageRequest, MessageResponse> handler //型情報から先程のMessageHandlerのインスタンスを受け取ることが出来る。 public MessageController(IRequestHandler<MessageRequest,MessageResponse> handler) { this.handler = handler; } [Get] public async ValueTask<MessageResponse> SendMessage(MessageRequest req) { var response = handler.Invoke(req); return response; } } この例はSendMessageがコールされたときに、送られたメッセージをハンドリングしてレスポンスを返すというケースで、MessageHandlerがあることにより、API側はそれを呼んで返すだけ、となっています。 メッセージのやり取りを経由するシンプルなサーバーであっても、ユーザー情報や、グループの情報など、多くの依存やビジネスロジックが絡んでくるかと思います。それらを隠蔽しつつ、「リクエストからレスポンス」と単純なインターフェースへ統一出来ることには価値があるのでは思います。なるほど、これがMediatorパターンか(多分違う) Request/Response All, Asyncについて 非同期版のAsyncについてはPub/Subの場合とほとんど同じ流れでいけます。名前にAsyncが付き、Requestの戻り値はValueTask<TResponse>になります。 ですが、RequestAllHandlerというものがあり、これについては言及しておいた方が良さそうです。 複数のハンドラを実行したい場合、I(Async)?RequestAllHandlerとしてインスタンスを受け取ることで、DIに登録されているすべてのハンドラを実行することが出来ます。 インターフェースは以下のようになっています。 public interface IRequestAllHandler<in TRequest, out TResponse> { TResponse[] InvokeAll(TRequest request); IEnumerable<TResponse> InvokeAllLazy(TRequest request); } public interface IAsyncRequestAllHandler<in TRequest, TResponse> { ValueTask<TResponse[]> InvokeAllAsync(TRequest request, CancellationToken cancellationToken = default); ValueTask<TResponse[]> InvokeAllAsync(TRequest request, AsyncPublishStrategy publishStrategy, CancellationToken cancellationToken = default); IAsyncEnumerable<TResponse> InvokeAllLazyAsync(TRequest request, CancellationToken cancellationToken = default); } IRequestHandler自体の実装は変わりませんが、複数のインスタンスが必要なので、以下のようなハンドラもあったとしましょう。 ///送られてきたメッセージを別のサーバーに転送する public class MessageTransferHandler: IRequestHandler<MessageRequest,MessageResponce> { //詳細略 } 例の用途はともかくとして、このようなケースはあるはずでしょう。 これらを、使用側で、IRequestAllHandlerとして受けます。 ctor(IRequestAllHandler<MessageRequest,MessageResponse> handlers) { this.handlers = handlers; } 呼び出し箇所で、InvokeAllまたは、InvokeAllLazyを呼び出すことが出来ます。Lazyの方はその名の通り遅延実行でforeachで回すまで実行されません。ちなみにAsyncの方だと、レアキャラの(?)AsyncEnumerableを使うことが出来ます! MessageResponse[] responses = handlers.InvokeAll(request); //or IEnumerable<MessageResponse> responses = handlers.InvokeAllLazy(request); 同じインターフェースの実装を複数DIに登録するパターンに慣れない方もいるかも知れません(私がそうでした) 複数を登録した場合にどうなるのか、気になって調べてみたところ(これはDIの実装に依るのですが)私の知る限りではMSのDIとVContainerは最後に登録されたものが渡されるようです。結果は未定義では無いということですね。RequestAllHandlerに関しては、実行順序はおそらく関知するところでは無いのでしょう。 Filter Pub/Subと同様、RequestHandlerにもFilterを挟むことが出来ます。 文字列のリクエストをすべて反転させてしまうはた迷惑なフィルターなら以下のように作れるでしょう。 using System.Linq; public class ReverseStringFilter: RequestHandlerFilter<string,string> { public override string Invoke(string request, Func<string,string> next) { return string.Concat(request.Reverse()); } } Async版。ValueTaskとCancellationTokenが加わる。 public class AsyncStringReverseFilter : AsyncRequestHandlerFilter<string, string> { public override async ValueTask<string> InvokeAsync(string request, CancellationToken cancellationToken, Func<string, CancellationToken, ValueTask<string>> next) { return string.Concat(request.Reverse()); } } RequestHandlerのFilterは、アトリビュートが基本となります。 [RequestHandlerFilter(typeof(ReverseStringFilter))] public class StringMessageHandler: IRequestHandler<string,string> { //略 } Asyncの場合は、 [AsyncRequestHandler]をつかいます。 今回はここまでです。 MessagePipeの主要な機能として、Redisを使ったPub/Subなどが出来ます! 続編は書く、鴨!です。鴨。 ここまで読んでいただきありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Selenium idやnameの分からない入力欄に入力

時間のない人は1.、6.、7.を読んで下さい。 1.ログイン 前回に引き続き、HTMLソースをまだ見れないうちに自動操作を記述するケースです。 ページ中の1番目の入力欄(input type="text")は、XPathで"//input[@type='text'][1]"で手に入ります。2番目なら[2]です。 HTML.html <form action="XXXXXXXX"> ユーザーID <input type="text" value="" /><br /> パスワード <input type="password" value="" /><br /> <input type="submit" value="ログイン" /> </form> C#.cs driver.FindElement(By.XPath("//input[@type='text'][1]")).SendKeys("abcde"); driver.FindElement(By.XPath("//input[@type='password'][1]")).SendKeys("fghij"); driver.FindElement(By.XPath("//input[@value='ログイン'] | //button[text()='ログイン']")).Click(); Java.java driver.findElement(By.xpath("//input[@type='text'][1]")).sendKeys("abcde"); driver.findElement(By.xpath("//input[@type='password'][1]")).sendKeys("fghij"); driver.findElement(By.xpath("//input[@value='ログイン'] | //button[text()='ログイン']")).click(); Python.py driver.find_element_by_xpath("//input[@type='text'][1]").send_keys("abcde") driver.find_element_by_xpath("//input[@type='password'][1]").send_keys("fghij") driver.find_element_by_xpath("//input[@value='ログイン'] | //button[text()='ログイン']").click() 2.テーブルなし登録画面(失敗) 下記のようなHTMLの場合に、「氏名」「住所」の「文字列の右」という指定をして、tableを含むかどうか分からない場合にも要素を取得できないかがんばったのですが、できませんでした。 //[contains(text(),'氏名')] でformを見つけられ、 //[contains(text(),'氏名')]//input[@type='text'] で氏名の入力欄も見つけられますが、「住所」にそのまま置き換えてもだめで、 //[contains(text()[2],'住所')] とする必要がある上に、これで手に入るのもformなので、結局inputにも[2]が必要になり、 //[contains(text()[2],'住所')]//input[@type='text'][2] となります。 inputに[2]を指定するのであれば、 //*[contains(text()[2],'住所')] 自体が必要ありません。 また、tableを使用せずにform直書きと分かっていないとtext()に[2]を使えません。 HTML.html <form action="XXXXXXXX"> 氏名 <input type="text" name="name" value="" /><br /> 住所 <input type="text" name="address" value="" /><br /> <input type="submit" value="登録" /> </form> 3.テーブルあり登録画面(途中経過) 上のHTMLだと見出しがずれるので使われないで、下記のようにtableが使われてtdでそれぞれ別になれば、 //[contains(text(),'氏名')]/..//input[@type='text'] でいけそうな気がしてきました。 上の記述だと、「氏名」の兄弟タグであれば左側のinputも取得してしまうので、さらに厳密にすると、 //[contains(text(),'氏名')]/following-sibling:://input[@type='text'] となります。 //[contains(text(),'氏名')]/following-sibling::[1]//input[@type='text'] で、次の兄弟タグのみになります。 //[contains(text(),'氏名')]/following-sibling::*//input[@type='text'][1] で、次の兄弟タグに限らず、兄弟タグの中で最初のinput type="text"になります。 HTML.html <form action="XXXXXXXX"> <table> <tbody> <tr><td>氏名</td><td><input type="text" name="name" /></td></tr> <tr><td>住所</td><td><input type="text" name="address" /></td></tr> <tr><td colspan="2"><input type="submit" value="登録" /></td></tr> </tbody> </table> </form> Python.py driver.find_element_by_xpath("//*[contains(text(),'氏名')]/following-sibling::*//input[@type='text'][1]").send_keys("佐藤太郎") driver.find_element_by_xpath("//*[contains(text(),'住所')]/following-sibling::*//input[@type='text'][1]").send_keys("東京都千代田区") driver.find_element_by_xpath("//input[@value='登録'] | //button[text()='登録']").click() HTMLが見れるようになれば、素直にname属性で探せばよいですが。 4.テーブルなし登録画面(リベンジ)(途中経過) 下記のXPathで見つけられました。 inputが階層下にある場合は未対応です。 HTML.html <form action="XXXXXXXX"> 氏名 <input type="text" name="name" value="" /><br /> 住所 <input type="text" name="address" value="" /><br /> <input type="submit" value="登録" /> </form> Python.py driver.find_element_by_xpath("//text()[contains(.,'氏名')]/following-sibling::input[@type='text'][1]").send_keys("佐藤太郎") driver.find_element_by_xpath("//text()[contains(.,'住所')]/following-sibling::input[@type='text'][1]").send_keys("東京都千代田区") driver.find_element_by_xpath("//input[@value='登録'] | //button[text()='登録']").click() 同じXPathでテーブルなしとテーブルあり両方に対応したいですが、このXPathだとテーブルありのHTMLでは見つけられませんでした。 5.テーブルあり、テーブルなし両対応(途中経過) 単純に「|」で結合しました。 Python.py driver.find_element_by_xpath("//*[contains(text(),'氏名')]/following-sibling::*//input[@type='text'][1] | //text()[contains(.,'氏名')]/following-sibling::input[@type='text'][1]").send_keys("佐藤太郎") driver.find_element_by_xpath("//*[contains(text(),'住所')]/following-sibling::*//input[@type='text'][1] | //text()[contains(.,'住所')]/following-sibling::input[@type='text'][1]").send_keys("東京都千代田区") driver.find_element_by_xpath("//input[@value='登録'] | //button[text()='登録']").click() 6. テーブルあり、テーブルなし(階層なし)、テーブルなし(階層下)全対応(最終形) following-sibling::*// と無駄なことをせずに、 following:: でいけました。 //text()[contains(.,'住所')] で「住所」を含むテキストノードを取得し、 /following:: を追加してそれ以降の兄弟ノードとそれらの子孫ノードを対象とし、 input[@type='text'] を追加してinput type="text"を複数取得し、 [1] を追加してその複数のうちの1番目を取得しています。 HTML.html <form action="XXXXXXXX"> <table> <tbody> <tr><td>氏名</td><td><input type="text" name="name" /></td></tr> <tr><td>住所</td><td><input type="text" name="address" /></td></tr> <tr><td colspan="2"><input type="submit" value="登録" /></td></tr> </tbody> </table> </form> または <form action="XXXXXXXX"> 氏名 <input type="text" name="name" value="" /><br /> 住所 <input type="text" name="address" value="" /><br /> <input type="submit" value="登録" /> </form> または <form action="XXXXXXXX"> 氏名 <div><input type="text" name="name" value="" /></div><br /> 住所 <div><input type="text" name="address" value="" /></div><br /> <input type="submit" value="登録" /> </form> Python.py driver.find_element_by_xpath("//text()[contains(.,'氏名')]/following::input[@type='text'][1]").send_keys("佐藤太郎") driver.find_element_by_xpath("//text()[contains(.,'住所')]/following::input[@type='text'][1]").send_keys("東京都千代田区") driver.find_element_by_xpath("//input[@value='登録'] | //button[text()='登録']").click() 7.XPathの確認方法 Google Chromeの開発者ツール(F12)のコンソールで$x("<XPathの内容>")を入力して確認できます。 結果のinputの左の三角を開けば、nameを確認できます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む