20200801のC#に関する記事は8件です。

WinnAppDriverの要素取得速度 - どちらが速い?

要旨

 様々なツールの登場によって、ソフトウェアテストの自動化がどうも容易になってきたらしい。つまり、作業者が退社して出社する間にテストをPCにやらせることが出来る。とは言っても時間は有限である。
社員「このボタンを押せば全てのテストが完了します」
社長「時間はどのくらい掛かる?」
社員「10年です」
は流石にあほ(実際、自動テストは人間の代わりにテストの大部分をやってくれますが、人間よりは遅い印象。あくまで省力化であって時短テクではないですね)。
時間短縮出来る部分は短縮すべきだろう。特に、対象のソフト内の要素(ボタンであったりマス目であったり)を取得するステップは1つのテスト内で大きな時間を要するだろう。
 この要素取得はおおまかに分けて3通り考えられる。

  • [案1] その要素を名前やAutomationIDなど、要素と一対一対応する値で取得する方法(下の絵, 案1)
  • [案2] 要素の系統樹(inspect.exeで見られるアレ)を上からたどるように1つずつ取得して、最終的に目的の要素を取得する方法(下の絵, 案2)
  • [案3] 予め一度だけ数段上の要素を取得しておき、その要素の下で目的の要素を取得する方法(下の絵, 案3)

 探索範囲で見れば、案1>案3>案2の順に大きいだろうから、案2が最速な気がする。では実際どうか?
 この記事では、Windows Application Driver (WinAppDriverとも)による自動テストの大部分を占めるソフトの要素取得に要する時間が、その取得法によってどう変化するかを調べた結果を示す。試した結果、中間の要素を1段ずつ取得する案2は案1よりわずかに遅くなり、案3が圧倒的に最速なことが分かった。案2では所要時間を改善できない上に細かすぎるため、ソフトのアップデートで中間要素が変わった場合にメンテナンスが苦行になるという点で有用と言えない。また、GUIのアップデートに対応するのがそんなに苦ではないラインを見極めて案3を取るのが良いだろう。GUIのアップデートが激しい開発中の段階ならば、開発チームにAutomationIDの付与をお願いして案1を取るのが良いだろう。

QiitaWinAppDriver.png

変更履歴

(飲酒執筆をしているので文体の統一が出来ていません)
2020年8月1日 : 初稿
2020年8月2日 : ほぼ全部変えた

環境

  • テスト対象のソフト : Excel
  • テストコードはC#で記述してテスト実行にはNUnit
  • WinAppDriver v1.2
  • NuGetから
    • Microsoft.WinAppDriver.Appium.WebDriver - WinAppDriver動かすため
    • NUnit - テスト実行
    • Nunit3TestAdapter - テスト実行

 ぶっちゃけMicrosoft.WinAppDriver.Appium.WebDriverはAppium v.3系を使ってて古いです。ですが環境構築が楽なのでそこはご愛敬。
 コード作成と実行はVisual Studioを使用した。

検証手順

 ソフトウェアテストのデモとして、Excelの「中央揃えボタン」を取得してクリックするテストを実行する。
その取得法を上記の3種類で行った。所要時間を比較するため、要素取得のステップを10回繰り返している。
 テストを行うためのセッション形成はSession.cs、テストの実体はTest.csである。コードは後に示す。
 実は上の図が、今回実験で行う実際の要素取得手順である。Testメソッド名も案1~3にしてある。

 Excelとのセッション形成は、WinAppDriverのサンプルコードを参考にして

Session.cs
using System;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Remote;

namespace GetElementSpeedTest
{
    public class Session
    {
        private const string winAppDriverURL = "http://127.0.0.1:4723";

        protected static WindowsDriver<WindowsElement> desktopSession;
        protected static WindowsDriver<WindowsElement> session;

        public static void Setup()
        {
            if (session == null)
            {
                // Desktopとsessionとる
                DesiredCapabilities desktopCapabilities = new DesiredCapabilities();
                desktopCapabilities.SetCapability("app", "Root");
                desktopSession = new WindowsDriver<WindowsElement>(new Uri(winAppDriverURL), desktopCapabilities);

                WindowsElement excelWindow = desktopSession.FindElementByName("Book1 - Excel");
                string excelTopLevelWindowHandle = (int.Parse(excelWindow.GetAttribute("NativeWindowHandle"))).ToString("x");

                // 起動済みExcelとsessionとる
                DesiredCapabilities appCapabilities = new DesiredCapabilities();
                appCapabilities.SetCapability("appTopLevelWindow", excelTopLevelWindowHandle);
                session = new WindowsDriver<WindowsElement>(new Uri(winAppDriverURL), appCapabilities);

                // Set implicit timeout to 1.5 seconds to make element search to retry every 500 ms for at most three times
                session.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1.5);
            }
        }


        public static void TearDown()
        {
            // Close the application and delete the session
            if (session != null)
            {
                session = null;
            }
        }
    }
}

とした。
そしてテスト実行のコードは

Test.cs
using System;
using OpenQA.Selenium.Appium;
using System;
using OpenQA.Selenium.Appium;
using NUnit.Framework;

namespace GetElementSpeedTest
{
    [TestFixture]
    public class Test : Session
    {
        [Test]
        public static void ConstTest()
        {
            Setup();
            AppiumWebElement mainWindow = session.FindElementByName("Book1 - Excel");
            AppiumWebElement ribbon = mainWindow.FindElementByName("下リボン");
            AppiumWebElement targetButton = mainWindow.FindElementByName("中央揃え");

            // begin speed test
            // end speed test

            targetButton.Click();

            TearDown();
        }

        [Test]
        public void 1()
        {
            Setup();
            AppiumWebElement mainWindow = session.FindElementByName("Book1 - Excel");
            AppiumWebElement ribbon = mainWindow.FindElementByName("下リボン");
            AppiumWebElement targetButton = mainWindow.FindElementByName("中央揃え");

            // begin speed test
            for (int i = 0; i < 10; i++)
            {
                targetButton = mainWindow
                    .FindElementByName("中央揃え");
            }
            // end speed test

            targetButton.Click();

            TearDown();
        }

        [Test]
        public void 2()
        {
            Setup();
            AppiumWebElement mainWindow = session.FindElementByName("Book1 - Excel");
            AppiumWebElement ribbon = mainWindow.FindElementByName("下リボン");
            AppiumWebElement targetButton = mainWindow.FindElementByName("中央揃え");

            // begin speed test
            for (int i = 0; i < 10; i++)
            {
                targetButton = mainWindow
                    .FindElementByName("下リボン")
                    .FindElementByName("ホーム")
                    .FindElementByName("配置")
                    .FindElementByName("中央揃え");
            }
            // end speed test

            targetButton.Click();

            TearDown();
        }

        [Test]
        public void 3()
        {
            Setup();
            AppiumWebElement mainWindow = session.FindElementByName("Book1 - Excel");
            AppiumWebElement ribbon = mainWindow.FindElementByName("下リボン");
            AppiumWebElement targetButton = mainWindow.FindElementByName("中央揃え");

            // begin speed test
            for (int i = 0; i < 10; i++)
            {
                targetButton = ribbon.FindElementByName("中央揃え");
            }
            // end speed test

            targetButton.Click();

            TearDown();
        }
    }
}

である。ConstTest()案n()($n=1, 2, 3$)の動作時間から共通部分に掛かる時間を差し引くための物である。

結果

 実行時間はVisual Studioのテストエクスプローラーに表示された物を使用した。結果は下表の通り。試行回数は3である。単位は秒である。

案1 案2 案3
0.86 ± 0.13 0.95 ± 0.11 0.073 ± 0.012

 つまり、予想に反して案3が1桁少ない高速さを誇り、案2がわずかな差ではあるが最も遅かった。

結語

 やはり最初に探索範囲を絞ってから要素を取得した案3が爆速であった。だからといって、Findの度に中間要素を細々取得する案2は改善しない上に保守性が劣悪なので悪手だろう。多分FineBy系のメソッドはそれ自体が時間を食うから案2がダメだったんでしょうね。案1は確かに遅いが、AutomationIDがあったり、Nameが奇跡的にuniqueで多言語対応しなくて良いならGUIの変更に対して最強である(このNameに関する仮定は万に一つ無いだろうが)。
 しかし、案2を取らざるを得ない状況が存在する。目的要素にAutomationIDが無く、その他の値でも一意に取得できない時である。そんなときはさっさとAutomatinIDを付けましょう。たとえその要素の使用頻度が低くても。

 要旨のセクションにも書いたが、中間要素をいっぱい取る気遣いをしたところで、そのボタンの場所が階層レベルで変更されたら即死するので使い物にならなくなる。ソフトのGUIはバンバン変更される物だろうから、メンテに時間を掛けなくて良いようにやっていきましょう。この塩梅をうまく見極めて案3を使えるのが理想だろう。それでも仕様変更が盛んだがテストもやらなきゃいけない佳境(やさいいことば)において役立つのがAutomationIDだ。このメリットに言及している記事は星の数ほどありますが、1要素に1つずつ設定できること、Nameとは違ってlocalization対応をしなくて良いこと、この二つだけで多くの人命が救われる。そう、多くの。。。ヤバいときはまずAutomationID頼りで案1で行くのが堅いだろう。案3をとって日々最新版に合わせる作業をするか、落ち着いた頃に案3に切り替えるの、どっちが楽でしょうね。でも自動テストコードのデバッグが激烈遅いと生産性も集中力もそがれるからな。。。
 ということで、実行速度は
 案3>>案1>案2 の順で速く、
 保守性は
 案1>>案3>>案2 の順で良いと考えられる。

 以上、非常に簡単ではありますが報告とさせて頂きます。この記事が一人でも多くの「要素を一段階ずつ拾った方が速いんじゃね?」と考えた人の出鼻をくじき、実証実験の(無駄)手間を省ければ幸いです。その人こそが私なんですけどね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

System.Windows.Mediaを使うときの設定

ソリューションエクスプローラーの「参照」を右クリックして、
「参照の追加」をクリック
「PresentationCore」を追加

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity(C#)でWINAPIを使う

はじめに

Windowsのデスクトップアプリで枠(BORDER)とかタイトルバーを消したいというのはよくあることですが、WINAPIのドキュメントを引用しながら方法を教えてくれるページがそんなに無いようなので作ることにしました。つまりは下記あたりを見るんですね。
https://docs.microsoft.com/en-us/windows/win32/api/winuser/

そもそもQiitaでWINAPI(WindowsAPI)のタグがない? みんな使っていると思うんですが興味ないんですかね……。
だらだら説明を書くので、方法だけを知りたい人は別のページをググると良いです。

背景

「NICT インターネット時刻供給サービス」を利用するUnity(2018.4.24f1)製のデスクトップ時計を作成しました。
https://github.com/hakua-doublemoon/NetClock
なんでこんなものを作ったかといえば、私の富士通製タブレット型PCの時計が半年に一回ぐらい狂いまくる時期が来るからです。(RTCとかがおかしいのかな……)
ついでについなちゃんの声で時報してもらってます。

環境

項目名
OS Windows10
Unity 2018.4.24f1

内容

基本的な考え方

Windowsのアプリケーションですので、WindowsのAPIを使ってWindow(=アプリの表示)を変えることができるようです。
このWINAPIは基本的にC/C++のコードですが、DLLImportすることでC#でも使えます。DLLImportについては他の記事が詳しいのではと思います。(これに関してはもう調べてない。)

事前: Windowの取得

BorderやTitleの表示はWindowの属性として決まっています。なのでWindowを取得し、現在の属性を取得し、必要な部分だけ変更して設定しなおします。

Window(ハンドラー)の取得: FindWindow

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-findwindowa
とにかく第二引数に指定した名前のWindow(アプリ)を見つけたい場合は第一引数をNULLにするようです。

WindowController.cs
    [DllImport("user32.dll", EntryPoint = "FindWindow")]
    public static extern IntPtr FindWindow(System.String className, System.String windowName);

    string windowName = "net_clock"; // Unityで設定するアプリ名とそろえる。
    var window = FindWindow(null, windowName);

Window属性の取得: GetWindowLong

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlonga
第一引数で指定したハンドラーのWindowの属性(情報)を取得します。ただしWindowの属性は何種類もあり、内部仕様があるのか知りませんが32bitで収まっていません。そこで第二引数で取得/変更したい属性のインデックスを指定します。
(すなわち、特定の属性を変更したい場合はそれに対応したIndexで取得、設定する必要があるということです)

WindowController.cs
    [DllImport("user32.dll")]
    public static extern int GetWindowLong(IntPtr hWnd, int nIndex);

ほかのAPIについてはそれぞれのユースケースに沿って述べていくことにします。

枠(BORDER)とタイトルバーを消す

BORDERの属性: WS_CAPTION

BORDERの属性があるのでそれを変更します。すなわち下記です:

https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles

WS_CAPTION 0x00C00000L
The window has a title bar (includes the WS_BORDER style).

WS_BORDER と WS_DLGFRAME を合わせてこれにしているコードが散見されますが、あまり意味がないようです。
上のGetWindowLongで書きましたが、Windowの属性は多数あり、取得・設定する際はそれに対応したIndexを使用する必要がありますが、どれなのかはGetWindowLongなどの説明に書いてあります。

GWL_STYLE -16
Retrieves the window styles.

したがって下記のようなコードで取得ができます。

WindowsController.cs
            const int GWL_STYLE = -16;
            int style = GetWindowLong(window, GWL_STYLE);

属性の変更: SetWindowLong

属性の変更は、現在の属性をGetWindowLongで取得し、SetWindowLongで一部変更したものを設定しなおします。

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlonga

説明のとおり、引数はGetWindowLongと同様で、第三引数に設定する値を渡します。

WindowsController.cs
    [DllImport("user32.dll")]
    public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

            style &= ~WS_CAPTION;
            SetWindowLong(window, GWL_STYLE, style);

Window位置の変更: SetWindowPos

BORDERを消すと容易にWindowの位置を変えられなくなります。そこでSetWindowPosで適当な位置に移動させてやります。

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos

第二引数はWindowの順序の操作のようです。こだわりがなければHWND_TOPでよいでしょう。前面に出てきます。
第七引数は(ざっくり言うと)細かい動きを制御できるようです。特にこだわりがなければ0でよさそうです。

WindowsController.cs
    [DllImport("user32.dll", EntryPoint = "SetWindowPos")]
    private static extern bool SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int x, int Y, int cx, int cy, int wFlags);

            const int HWND_TOP = 0;
            SetWindowPos(window, HWND_TOP, x, y, width, height, 0);

ここまででこういう感じなります。

image.png

背景を透明にする

せっかく枠を消したら背景も消したくなるでしょう。背景の削除は下記の手順で行います。

  1. Layered Window属性の設定
  2. 背景色へのAlpha値の適用

Layered Window属性: WS_EX_LAYERED

https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles

WS_EX_LAYERED 0x00080000
The window is a layered window. This style cannot be used if the window has a class style of either CS_OWNDC or CS_CLASSDC.

Layered Windowについて:
https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#layered-windows

visual effects for a window ... wishes to use alpha blending effects. The system automatically composes and repaints layered windows and the windows of underlying applications
Alpha Blending 効果を使用したいウィンドウの視覚効果。システムは自動的にLayered Windowと下にあるアプリケーションのウィンドウを構成し再描画します。

これだって感じですね。WS_EX_LAYERED属性もSetWindowLongで設定できますが、Indexは-20になります。

GWL_EXSTYLE -20
Sets a new extended window style.

WindowsController.cs
            const int GWL_EXSTYLE = -20;
            const int WS_EX_LAYERED = 0x80000;

            int style = GetWindowLong(window, GWL_EXSTYLE);
            SetWindowLong(window, GWL_EXSTYLE, style | WS_EX_LAYERED);

Layered Windowの属性の設定: SetLayeredWindowAttributes

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setlayeredwindowattributes
第三引数が難しくて正直に言って理解できていません。
単純に背景色を透明にするだけなら、第四引数をLWA_COLORKEYにすれば、第二引数(crKey)を透明な色としてくれます。つまりUnityの方で背景色を適当な色にして、そのカラーコードを第二引数に書けばいいです。

WindowsController.cs
    [DllImport("user32.dll", EntryPoint = "SetLayeredWindowAttributes")]
    private static extern Boolean SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags);

            const int LWA_COLORKEY = 1;
            SetLayeredWindowAttributes(window, 0x00000000, 0, LWA_COLORKEY);

これでこうなります。

image.png


いじょ。
この記事を書きながら改めて各APIを調査し、コードを整理できました。やはり正しい知識が妥当なコードを書く鍵だと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#のマイクロサービス開発ツール "Project Tye" を試す

C# / .NET Coreを使ったマイクロサービス開発を支援するツール Project Tye がリリースされました。
ASP.NETの開発チームが公開した下記のブログ記事を実際に試してみた内容を記載します。
https://devblogs.microsoft.com/aspnet/introducing-project-tye/

どういうツール?

Project Tyeは、マイクロサービスと分散アプリケーションの開発、テスト、デプロイを容易にするツールです。
ただし現在は実験的な(experimental)開発者ツールとして提供されているため、今後機能の追加、変更、廃止が行われる可能性はあります。そのため、この投稿は2020年8月1日時点の情報となっています。

ソースコードはこちらのGitHubリポジトリで公開されています。
dotnet/tye

Project Tye でできること

  1. マイクロサービスの開発を容易にする
    • 1つのコマンドで多くのサービスを実行する
    • コンテナでの依存関係の使用
    • 単純な規則を使用して他のサービスのアドレスを発見する
  2. .NETアプリケーションのKubernetesへのデプロイメントの自動化
    • .NETアプリケーションの自動コンテナ化
    • 最小限の知識または構成でKubernetesマニフェストを生成する
    • 単一の構成ファイルを使用する

Project Tyeを使うことで専用のダッシュボードが利用可能となり、マイクロサービス構成の各アプリケーションを閲覧することもできます。
2020-08-01_17h04_07.png

インストール方法

前提としてProject Tyeには .NET Core 3.1 が必要なので、予めインストールしておきましょう。
https://dotnet.microsoft.com/download

Project Tyeは .NET Coreのグローバルツールとして利用することができます。下記のコマンドでインストールします。

$ dotnet tool install -g Microsoft.Tye --version "0.2.0-alpha.20258.3"

正しくインストールできたことを確認するには、下記のコマンドを実行します。

$ dotnet tool list -g
パッケージ ID                              バージョン                    コマンド
-------------------------------------------------------------------------
microsoft.tye                         0.2.0-alpha.20258.3      tye

サンプルアプリケーションを作ってみる

フロントエンド

まずフロントエンドのアプリケーションとして、ASP.NET Core Razor Pagesのプロジェクトを作成します。

$ mkdir microservices
$ cd microservices
$ dotnet new razor -n frontend

The template "ASP.NET Core Web App" was created successfully.
This template contains technologies from parties other than Microsoft, see https://aka.ms/aspnetcore/3.1-third-party-notices for details.

Processing post-creation actions...
Running 'dotnet restore' on frontend\frontend.csproj...
  復元対象のプロジェクトを決定しています...
  D:\src\yuta\netcore\microservices\frontend\frontend.csproj を復元しました (221 ms)。

Restore succeeded.

作ったばかりのプロジェクトに対してTyeを実行してみます。

$ tye run frontend
Loading Application Details...
Launching Tye Host...

[17:16:18 INF] Executing application from D:\src\yuta\netcore\microservices\frontend\frontend.csproj
[17:16:18 INF] Dashboard running on http://127.0.0.1:8000
[17:16:18 INF] Building projects
[17:16:24 INF] Launching service frontend_03aff344-7: D:\src\yuta\netcore\microservices\frontend\bin\Debug\netcoreapp3.1\frontend.exe
[17:16:24 INF] frontend_03aff344-7 running on process id 25512 bound to http://localhost:59457, https://localhost:59458
[17:16:24 INF] No process was selected. Waiting.
[17:16:25 INF] Selected process 25512.
[17:16:25 INF] Listening for event pipe events for frontend_03aff344-7 on process id 25512

tye runコマンドでは、指定したアプリケーションの実行が行われます。
その際、Tyeのダッシュボードも同時に実行されるため、http://127.0.0.1:8000にアクセスするとフロントエンドのアプリケーションがサービスとして登録されていることがわかります。
2020-08-01_17h20_18.png

表のBindings列に表示されているのがフロントエンドアプリケーションにアクセスするためのURLです。アクセスすると実際にページを閲覧することができます。
2020-08-01_17h21_59.png

また、表のLogs>Viewにアクセスすることでアプリケーションのログを閲覧することもできます。ここで表示されるログは標準出力に流れるログです。
2020-08-01_17h25_13.png

バックエンドサービスを追加する

次にバックエンドサービスとしてASP.NET Core WebAPIのアプリケーションとソリューションファイルを作成します。

$ dotnet new webapi -n backend
$ dotnet new sln
$ dotnet sln add frontend backend

作成したソリューションファイルに対してTyeを実行してみます。
ソリューションファイルに含まれるフロントエンドとバックエンドのアプリケーションが起動します。

$ tye run
Loading Application Details...
Launching Tye Host...

[17:29:00 INF] Executing application from D:\src\yuta\netcore\microservices\microservices.sln
[17:29:00 INF] Dashboard running on http://127.0.0.1:8000
[17:29:00 INF] Building projects
[17:29:06 INF] Launching service backend_cf382a78-c: D:\src\yuta\netcore\microservices\backend\bin\Debug\netcoreapp3.1\backend.exe
[17:29:06 INF] Launching service frontend_be0c1aa4-d: D:\src\yuta\netcore\microservices\frontend\bin\Debug\netcoreapp3.1\frontend.exe
[17:29:06 INF] backend_cf382a78-c running on process id 28564 bound to http://localhost:59884, https://localhost:59885
[17:29:06 INF] No process was selected. Waiting.
[17:29:06 INF] frontend_be0c1aa4-d running on process id 2740 bound to http://localhost:59882, https://localhost:59883
[17:29:06 INF] No process was selected. Waiting.
[17:29:06 INF] Selected process 28564.
[17:29:06 INF] Listening for event pipe events for backend_cf382a78-c on process id 28564
[17:29:06 INF] Selected process 2740.
[17:29:06 INF] Listening for event pipe events for frontend_be0c1aa4-d on process id 2740

ダッシュボードにも2つのサービスが登録されていることが確認できます。
2020-08-01_17h30_59.png

フロントエンドからバックエンドサービスを呼び出す

フロントエンドからバックエンドのWebAPIを呼び出し、APIが返すデータを画面に表示するようにします。
実際のC#コードは省略するため本家の記事を参照してください。

Tyeを通じてフロントエンドからバックエンドサービスを呼び出す上で、通常のASP.NET Coreの実装と異なる部分があリます。

まず、下記のコマンドでフロントエンドアプリケーションに拡張機能をインストールします。
https://www.nuget.org/packages/Microsoft.Tye.Extensions.Configuration/

$ dotnet add frontend/frontend.csproj package Microsoft.Tye.Extensions.Configuration --version "0.4.0-*"

次に、フロントエンドアプリケーションのStartup.csファイルのConfigureServicesメソッドにて、バックエンドサービスに通信するためのHTTPクライアントを登録します。

public void ConfigureServices(IServiceCollection services)
{
   services.AddRazorPages();

   // ここから
   services.AddHttpClient<WeatherClient>(client =>
   {
      client.BaseAddress = Configuration.GetServiceUri("backend");
   });
   // ここまで
}

GetServiceUriメソッドが拡張機能によって利用可能となり、指定した名前のサービスのURIを取得することができます。
https://github.com/dotnet/tye/blob/083a97f353069ef8a091651d4c9ca445c994b2e5/src/Microsoft.Tye.Extensions.Configuration/TyeConfigurationExtensions.cs#L11

一通りの実装を終えたら再びTyeを実行し、フロントエンドアプリケーションにアクセスするとバックエンドAPIが返すデータを表示することを確認できます。
2020-08-01_17h49_44.png

Tyeの構成管理ファイルを作成する

tye.yamlというYAMLファイルで構成を管理することができ、依存関係のカスタマイズも行うことができます。
まずは既存のソリューションファイルからtye.yamlを作成します。

$ tye init
Created 'D:\src\yuta\netcore\microservices\tye.yaml'.

tye.yamlをみると、フロントエンドとバックエンドがサービスとして登録されていることが確認できます。

# tye application configuration file
# read all about it at https://github.com/dotnet/tye
#
# when you've given us a try, we'd love to know what you think:
#    https://aka.ms/AA7q20u
#
name: microservices
services:
- name: frontend
  project: frontend/frontend.csproj
- name: backend
  project: backend/backend.csproj

tye.yamlのスキーマ構成についてはこちらに説明が載っています。
https://github.com/dotnet/tye/blob/master/docs/reference/schema.md

キャッシュ用のRedisを追加する

バックエンドAPIにRedisによるキャッシュを追加します。こちらも具体的なC#のコードについては本家の記事を参照してください。

ここで大事なのはバックエンドアプリケーションにおいて、Redisの接続文字列の扱い方です。
バックエンドアプリケーションのStartup.csファイルのConfigureServicesメソッドでは、このようにRedisの設定を行っています。

public void ConfigureServices(IServiceCollection services)
{
   services.AddControllers();

   // ここから
   services.AddStackExchangeRedisCache(o =>
   {
      o.Configuration = Configuration.GetConnectionString("redis");
   });
   // ここまで
}

"redis"という名前の接続情報を環境変数から取得して使用しています。(Configuration.GetConnectionStringメソッド)
ただしまだ"redis"という名前の環境変数はありませんので、tye.yamlを編集してRedisを構成します。

name: microservice
services:
- name: backend
  project: backend\backend.csproj
- name: frontend
  project: frontend\frontend.csproj
- name: redis
  image: redis
  bindings:
  - port: 6379
    connectionString: "${host}:${port}"
- name: redis-cli
  image: redis
  args: "redis-cli -h redis MONITOR"

RedisおよびRedis CLIをサービスとして追加しました。(3番目と4番目)
開発環境にインストールされたものを利用するのではなく、image部分にてDockerコンテナーとしてRedisとRedis CLIを用意する内容になっています。
またconnectionString: "${host}:${port}"にてRedisの接続情報を定義しています。

フロントエンド+バックエンド+Redis のアプリケーションを実行する

Tyeを実行するとtye.yamlに定義された構成に沿ってアプリケーションを実行します。
そのためDockerを予め起動しておく必要があります。

$ tye run
Loading Application Details...
Launching Tye Host...

[18:04:59 INF] Executing application from D:\src\yuta\netcore\microservices\tye.yaml
[18:05:00 INF] Dashboard running on http://127.0.0.1:8000
[18:05:01 INF] Docker image redis already installed
[18:05:01 INF] Creating docker network tye_network_527351f0-d
[18:05:01 INF] Running docker command network create --driver bridge tye_network_527351f0-d
[18:05:01 INF] Running image redis for redis_9c95ee87-2
[18:05:01 INF] Running image redis for redis-cli_deeaaa37-a
[18:05:01 INF] Running image mcr.microsoft.com/dotnet/core/sdk:3.1 for backend-proxy_88944fc9-7
[18:05:01 INF] Running image mcr.microsoft.com/dotnet/core/sdk:3.1 for frontend-proxy_314a0623-6
[18:05:01 INF] Building projects
[18:05:02 INF] Running container redis_9c95ee87-2 with ID 554f506efc4c
[18:05:02 INF] Running docker command network connect tye_network_527351f0-d redis_9c95ee87-2 --alias redis
[18:05:02 INF] Running container redis-cli_deeaaa37-a with ID 0911e9cca09a
[18:05:02 INF] Running docker command network connect tye_network_527351f0-d redis-cli_deeaaa37-a --alias redis-cli
[18:05:03 INF] Collecting docker logs for redis_9c95ee87-2.
[18:05:03 INF] Collecting docker logs for redis-cli_deeaaa37-a.
[18:05:04 INF] Launching service frontend_4fb62798-4: D:\src\yuta\netcore\microservices\frontend\bin\Debug\netcoreapp3.1\frontend.exe
[18:05:04 INF] Launching service backend_092bc562-4: D:\src\yuta\netcore\microservices\backend\bin\Debug\netcoreapp3.1\backend.exe
[18:05:04 INF] frontend_4fb62798-4 running on process id 27144 bound to http://localhost:61021, https://localhost:61022
[18:05:04 INF] No process was selected. Waiting.
[18:05:04 INF] backend_092bc562-4 running on process id 14672 bound to http://localhost:61019, https://localhost:61020
[18:05:04 INF] No process was selected. Waiting.
[18:05:05 INF] Selected process 27144.
[18:05:05 INF] Listening for event pipe events for frontend_4fb62798-4 on process id 27144
[18:05:05 INF] Selected process 14672.
[18:05:05 INF] Listening for event pipe events for backend_092bc562-4 on process id 14672
[18:05:06 INF] Running container frontend-proxy_314a0623-6 with ID 19322eff7c3f
[18:05:06 INF] Running docker command network connect tye_network_527351f0-d frontend-proxy_314a0623-6 --alias frontend
[18:05:06 INF] Running container backend-proxy_88944fc9-7 with ID 36240965761c
[18:05:06 INF] Running docker command network connect tye_network_527351f0-d backend-proxy_88944fc9-7 --alias backend
[18:05:06 INF] Collecting docker logs for frontend-proxy_314a0623-6.
[18:05:07 INF] Collecting docker logs for backend-proxy_88944fc9-7.

Tyeのダッシュボードを確認するとフロントエンド、バックエンドのサービスに加え、RedisとRedis CLIも登録されていることがわかります。
2020-08-01_18h06_22.png
2020-08-01_18h10_10.png

まとめ

本家の記事に沿って、簡単なマイクロサービスアプリケーションを実装してみました。
Project Tyeはまだ実験中のツールですが、ダッシュボードが使え、YAMLファイルでアプリケーションの依存関係を管理することができるため便利そうですね。

次回はこのアプリケーションをKubernetesにデプロイしてみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#]Jsonシリアライズ サンプルコード

主題

Json.NETライブラリを使用したJSONシリアライズサンプルコードを備忘録として記載。

Json.NET

https://www.newtonsoft.com/json

単一オブジェクトのシリアライズ

public static string SerializeObject()
{
    Product product = new Product();
    product.Name = "Apple";
    product.Expiry = new DateTime(2008, 12, 28);
    product.Price = 3.99M;
    product.Sizes = new string[] { "Small", "Medium", "Large" };
    product.list = new List<int>(); product.list.Add(1); product.list.Add(2); product.list.Add(3);

    return JsonConvert.SerializeObject(product, Formatting.Indented);

}
{
  "Name": "Apple",
  "Expiry": "2008-12-28T00:00:00",
  "Price": 3.99,
  "Sizes": [
    "Small",
    "Medium",
    "Large"
  ],
  "list": [
    1,
    2,
    3
  ]
}

メンバに配列やコレクションがあっても問題なく動作する。
ちなみに、publicメンバ以外はシリアライズされないことに注意。(実際これではまった)

一次元コレクションのシリアライズ

public static string SerializeSingleCollection()
{
    Product product1 = new Product();
    product1.Name = "Apple";
    product1.Expiry = new DateTime(2008, 12, 28);
    product1.Price = 3.99M;
    product1.Sizes = new string[] { "Small", "Medium", "Large" };
    product1.list = new List<int>(); product1.list.Add(1); product1.list.Add(2); product1.list.Add(3);

    Product product2 = new Product();
    product2.Name = "Orange";
    product2.Expiry = new DateTime(2008, 12, 28);
    product2.Price = 3.99M;
    product2.Sizes = new string[] { "Small", "Medium", "Large" };
    product2.list = new List<int>(); product2.list.Add(1); product2.list.Add(2); product2.list.Add(3);

    List<Product> products = new List<Product>();
    products.Add(product1);
    products.Add(product2);

    return JsonConvert.SerializeObject(products);

}

  {
    "Name": "Apple",
    "Expiry": "2008-12-28T00:00:00",
    "Price": 3.99,
    "Sizes": [
      "Small",
      "Medium",
      "Large"
    ],
    "list": [
      1,
      2,
      3
    ]
  },
  {
    "Name": "Orange",
    "Expiry": "2008-12-28T00:00:00",
    "Price": 3.99,
    "Sizes": [
      "Small",
      "Medium",
      "Large"
    ],
    "list": [
      1,
      2,
      3
    ]
  }
]

多次元コレクションのシリアライズ

公式のサンプルコードには載っていないが、
多次元コレクションの場合でも同じようにシリアライズ可能

private static List<Product> Products()
{
    Product product1 = new Product();
    product1.Name = "Apple";
    product1.Expiry = new DateTime(2008, 12, 28);
    product1.Price = 3.99M;
    product1.Sizes = new string[] { "Small", "Medium", "Large" };
    product1.list = new List<int>(); product1.list.Add(1); product1.list.Add(2); product1.list.Add(3);

    Product product2 = new Product();
    product2.Name = "Orange";
    product2.Expiry = new DateTime(2008, 12, 28);
    product2.Price = 3.99M;
    product2.Sizes = new string[] { "Small", "Medium", "Large" };
    product2.list = new List<int>(); product2.list.Add(1); product2.list.Add(2); product2.list.Add(3);

    List<Product> products = new List<Product>();
    products.Add(product1);
    products.Add(product2);

    return products;
}


public static string SerializeMultipleCollection()
{
    List<List<Product>> products_list = new List<List<Product>>();
    products_list.Add(Products());
    products_list.Add(Products());
    products_list.Add(Products());
    return JsonConvert.SerializeObject(products_list, Formatting.Indented);

}
[
  [
    {
      "Name": "Apple",
      "Expiry": "2008-12-28T00:00:00",
      "Price": 3.99,
      "Sizes": [
        "Small",
        "Medium",
        "Large"
      ],
      "list": [
        1,
        2,
        3
      ]
    },
    {
      "Name": "Orange",
      "Expiry": "2008-12-28T00:00:00",
      "Price": 3.99,
      "Sizes": [
        "Small",
        "Medium",
        "Large"
      ],
      "list": [
        1,
        2,
        3
      ]
    }
  ],
  [
    {
      "Name": "Apple",
      "Expiry": "2008-12-28T00:00:00",
      "Price": 3.99,
      "Sizes": [
        "Small",
        "Medium",
        "Large"
      ],
      "list": [
        1,
        2,
        3
      ]
    },
    {
      "Name": "Orange",
      "Expiry": "2008-12-28T00:00:00",
      "Price": 3.99,
      "Sizes": [
        "Small",
        "Medium",
        "Large"
      ],
      "list": [
        1,
        2,
        3
      ]
    }
  ],
  [
    {
      "Name": "Apple",
      "Expiry": "2008-12-28T00:00:00",
      "Price": 3.99,
      "Sizes": [
        "Small",
        "Medium",
        "Large"
      ],
      "list": [
        1,
        2,
        3
      ]
    },
    {
      "Name": "Orange",
      "Expiry": "2008-12-28T00:00:00",
      "Price": 3.99,
      "Sizes": [
        "Small",
        "Medium",
        "Large"
      ],
      "list": [
        1,
        2,
        3
      ]
    }
  ]
]

参考

https://stackoverflow.com/questions/13469765/how-to-deseralize-json-object-that-contains-multidimensional-array

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP.NET CoreでWeb開発~環境構築からHello World~

はじめに

なぜ今ASP.NET CoreでWeb開発なのか

これまでC#, ASPと言えばWindows OS(Azure)で動作させる製品のイメージでした。
しかし近年(2016~) .NETは、.NET Coreへ以降しつつあり、その際下記の大きな変化が見られました。

オープンソース化
リリース当初からGithubで公開された
クロスプラットフォーム
Windows OSはもちろん、LinuxやMacOSもサポートされた

これを機にC#でのWeb開発が広く普及していくと考えられます。

本記事のスコープ

  • .NET Coreのインストール
  • HelloWoldプロジェクト作成・実行

.NET Coreのインストール

1.MicroSoftのダウンロードページを開く
2.[Download .NET Core SDK]を選択し、インストーラをダウンロードする
3.インストーラを実行する
4.[インストール]を押下する(画像赤枠)
5.インストール完了まで待機
a.png

HelloWoldプロジェクト作成・実行

プロジェクト作成

※本記事ではVisual Studioのセットアップをスコープ対象外としております。
かなり多くの方が記事にしているので、セットアップされてない方は他記事を参考にしてください。

1.Visual Studioを開く
2.[新しいプロジェクトを作成]を選択する
b.png
3.[ASP.NET Core Webアプリケーション]を選択する
※選択肢にない場合は、Visual Studioインストーラで[ASP.NETとWeb開発]をインストールしましょう。
c.png
4.任意のプロジェクト名を設定し[作成]を選択する
d.png
5.アプリの種別を選択する、今回はHello Worldなので[空]を選択する
e.png
6.Visual StudioのEditorが開かれれば、プロジェクトの作成完了

Hellow World実行

1.Visual Studio上で[▶IIS Express]を選択する
f.png
2.デフォルトのソースでSSLを使用するよう実装されているため、[はい]を選択して自動生成される自己証明書を信頼する
1.png
3.[はい]を選択し、自動生成されたオレオレ証明書のインストールを行う
※発行者がご自身で開発中のアプリである事をしっかりと確認の上、自己責任で実施して下さい。
2.png
4.自動で開かれるブラウザ上でHello Worldが開かれたら成功
j.png

おわりに

今回は導入であるHello Worldでした。
次回から多くのフレームワークで採用されているMVCアプリケーションを作成したいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WPFで別ウィンドウを呼び出した時、別ウィンドウのタブ切り替えを指定したい

やりたいこと

  • メインウィンドウ
  • サブウィンドウ
  • サブウィンドウはタブ切り替えを持つ

メインウィンドウからサブウィンドウを開くとき、サブウィンドウのどのタブを開くのか指定したい

準備

メインウィンドウとサブウィンドウを用意します。

image.png

MainWindow.xaml
<Window><!-- ここは省略 -->
    <Grid>
        <StackPanel Margin="20">
            <Button Name="BtnA" Margin="20">SubWindow タブ AAA を開く</Button>
            <Button Name="BtnB" Margin="20">SubWindow タブ BBB を開く</Button>
            <Button Name="BtnC" Margin="20">SubWindow タブ CCC を開く</Button>
        </StackPanel>
    </Grid>
</Window>

image.png

SubWindow.xaml
<Window><!-- 省略 -->
    <Grid>
        <TabControl Margin="20">
            <TabItem Header="AAA" Name="AAA">
                <StackPanel>
                    <TextBox Margin="50">タブAAAです</TextBox>
                </StackPanel>
            </TabItem>
            <TabItem Header="BBB" Name="BBB">
                <StackPanel>
                    <TextBox Margin="50">タブBBBです</TextBox>
                </StackPanel>
            </TabItem>
            <TabItem Header="CCC" Name="CCC">
                <StackPanel>
                    <TextBox Margin="50">タブCCCです</TextBox>
                </StackPanel>
            </TabItem>
        </TabControl>
    </Grid>
</Window>

内容

タブ切り替え用のカウンタ変数を扱うクラス TabChange を用意します

TabChange.cs
public class TabChange
    {
        public static int tabChange;
    }

次はメインウィンドウの処理。ボタンを押すとタブ切り替え用のカウンタ変数 tabChange の値が変わります。

MainWindow.xaml.cs
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void BtnA_Click(object sender, RoutedEventArgs e)
    {
        TabChange.tabChange = 0;
        SubWindow sw = new SubWindow();
        sw.Show();
    }

    private void BtnB_Click(object sender, RoutedEventArgs e)
    {
        TabChange.tabChange = 1;
        SubWindow sw = new SubWindow();
        sw.Show();
    }

    private void BtnC_Click(object sender, RoutedEventArgs e)
    {
        TabChange.tabChange = 2;
        SubWindow sw = new SubWindow();
        sw.Show();
    }
}

次はサブウィンドウの処理。ウィンドウが生成された時のtabChangeの値によって、タブを選択します。

SubWindow.xaml.cs
/// <summary>
/// SubWindow.xaml の相互作用ロジック
/// </summary>
public partial class SubWindow : Window
{
    public SubWindow()
    {
        InitializeComponent();
        if (TabChange.tabChange == 1)
        {
            BBB_select();
        }
        if (TabChange.tabChange == 2)
        {
            CCC_select();
        }
    }
    public void BBB_select()
    {
        //BBBタブを選択状態にする
        BBB.IsSelected = true;
    }
    public void CCC_select()
    {
        //CCCタブを選択状態にする
        CCC.IsSelected = true;
    }
}

あとがき

「ウィンドウを開いてタブを選択する」メソッドを僕が知らないだけでしたらすみません。
まだまだ初心者なので、コード自体の書き方、qiitaへの文の書き方でこうしたらもっと読みやすくなる、良くなる、というご指摘がありましたら是非お願い致します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactiveProperty と ReactivePropertySlim ってどれくらい違うの?

ReactivePropertySlim のほうが早いよ。という説明はしましたが、どれくらい早いのか見てみましょう。
こういうとき便利なのが BenchmarkDotNet ですね。

ReactivePropertySlim の高速化のポイントの 1 つにインスタンス作成時のメモリアロケーションを最低限におさえるというのがあるように見えます(作ったの @neuecc さんなのでコード見てる雰囲気で…)。一方 ReactiveProperty はインスタンス作成時にちょっとした処理をしていたりします。では、どれくらい違うのか以下のようなベンチマークを書いてみました。

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Reactive.Bindings;
using System;
using System.Reactive.Subjects;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<ReactivePropertyBehcnmark>();
        }
    }

    public class ReactivePropertyBehcnmark
    {
        [Benchmark]
        public ReactiveProperty<string> ReactivePropertyDefaultConstructor() => new ReactiveProperty<string>();
        [Benchmark]
        public ReactivePropertySlim<string> ReactivePropertySlimDefaultConstructor() => new ReactivePropertySlim<string>();
    }
}

実行結果は以下の通りです。

|                                 Method |      Mean |     Error |    StdDev |
|--------------------------------------- |----------:|----------:|----------:|
|     ReactivePropertyDefaultConstructor | 90.127 ns | 1.2563 ns | 1.1752 ns |
| ReactivePropertySlimDefaultConstructor |  5.383 ns | 0.1262 ns | 0.1181 ns |

はい。平均で 18 倍ほど ReactiveProperty のほうが new しただけで遅いですね。これでも ReactiveProperty も早くなるように努力したのですが、これ以上は今の自分だと Slim に迫ることはできませんでした。

この他にもスレッドのディスパッチとかしてないのもあって値の設定とかでも Slim のほうが早いはずです。興味のある方は試してみるといいかも。

まとめ

早い。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む