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

[Azure Functions] 一定確率で少し待ってから応答を返す関数 (C#)

はじめに

LBの動作やアプリ側の挙動確認のため、一定の確率で応答に時間をかけてから返すサービスを作っておきたいなー…と思いたち、Azure Functions で簡単に作ってみました。

環境

  • .NET Core 3.1

コード

Visual Studio 2019 のテンプレートから HTTP Trigger で作成。

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace Random10SecFunction01
{
    public static class Function1
    {
        [FunctionName("Random10SecFunction01")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            int WAIT_TIME = 10; // seconds
            Random rnd = new Random(Environment.TickCount);

            if (rnd.Next(0,99) < 10)
            {
                log.LogInformation("Wait...");
                await Task.Delay(WAIT_TIME * 1000);
            }

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);

            string responseMessage = "This HTTP triggered function executed successfully.";

            return new OkObjectResult(responseMessage);
        }
    }
}

Usage

下記のようにアクセスすると、既定では90%の確率で即時応答、10%の確率で10秒間待ってレスポンスを返します。

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

【ドメイン駆動設計入門】を読んだ感想

はじめに

私は普段KotlinやJavaを使ってサーバーサイドの開発をしたり、アプリを開発したり、時にはフロントエンドを開発したりしています。

こちらの本を開発の時の設計に一助となればと思い読んでみました。

https://www.amazon.co.jp/dp/479815072X

感想

よかった点

  • 「ボトムアップでわかる!」と書いてあるだけあって、 理論先行ではなく具体例を挙げてひとつひとつ説明がありとても読みやすいです。
  • 途中で眠くなることなく一気に読み進めることができました。
  • DDDにおけるエンティティとクリーンアーキテクチャにおけるエンティティの違いについて言及されていたのがよかったです。このあたりの用語は開発の時に注意しなければと思いました。

よくなかった点

  • C# ならではと思う例などもあり、JavaやKotlinではどう変わるだろうか?と思いながら読みました。
  • 「筆者の見解では」というような記述があり、他の見解も幅広く集めて総合的に理解する必要があると感じました。どんな本でも鵜呑みは危険ですね。

おわりに

設計は本を読んだら急にできるようになるというものではないですね。日々の研鑽を積み重ね少しでも長持ちする設計を作っていきたいです。

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

UnityのMecanimアニメで、ムービープレーヤーのスライダーのような制御がしたい

やりたいこと

  • ムービープレーヤーのスライダーのような制御がしたい
    • アニメ全長の任意の位置へジャンプしたいです。
  • アニメの任意のフレームを再生したい
    • シーケンシャルな再生でなく、任意のフレームに対してランダムにアクセスして、スクリプト制御で正逆任意の速度で再生したいです。
検証環境
  • Unity 2019.4.x (LTS)

方法

具体例

以下のコードでは、対象のスライダーとアニメーターが、コードがアタッチされたオブジェクトの子孫の何処かに存在することを前提にしています。

AnimeSlider.cs
using UnityEngine;
using UnityEngine.UI;

public class AnimeSlider : MonoBehaviour {

    private Slider slider;
    private Animator animator;

    /// <summary>初期化</summary>
    void Start () {
        slider = GetComponentInChildren<Slider> ();
        slider.onValueChanged.AddListener (OnChangeSlider);
        animator = GetComponentInChildren<Animator> ();
        animator.speed = 0; // 再生をポーズ
    }

    /// <summary>スライダが変化</summary>
    public void OnChangeSlider (float _) {
        animator.Play (0, -1, slider.normalizedValue);
    }

}
  • これによって、スライダーの位置に応じた任意のフレームが再生されます。
  • ループするアニメの場合は、スライダの左端と右端で同じフレームが表示されます。
    • 同じにしたくない場合は、slider.normalizedValue * 0.9999999fとかにします。
  • ポーズを解除する場合は、animator.speed = 1;します。

蛇足

複数の同じアニメを時間差で表示
AnimeSlider.cs
using UnityEngine;
using UnityEngine.UI;

public class AnimeSlider : MonoBehaviour {

    private Slider slider;
    private Animator [] animators;

    /// <summary>初期化</summary>
    void Start () {
        slider = GetComponentInChildren<Slider> ();
        slider.onValueChanged.AddListener (OnChangeSlider);
        animators = GetComponentsInChildren<Animator> ();
        foreach (var animator in animators) {
            animator.speed = 0; // 再生をポーズ
        }
    }

    /// <summary>スライダが変化</summary>
    public void OnChangeSlider (float _) {
        for (var i = 0; i < animators.Length; i++) {
            var time = slider.normalizedValue - ((float) i / animators.Length);
            if (time < 0f) { time += 1f; }
            animators [i].Play (0, -1, time);
        }
    }

}


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

Azure DevOpsでSeleniumを使ったシステムテストがエラーの場合に画面キャプチャを見る方法

Azure DevOpsでSeleniumを使ったシステムテストがエラーの場合に画面キャプチャを見る方法

Seleniumを使ったシステムテストを作った場合、テストがエラーになった場合にその時の画面キャプチャが見られると便利です。特にAzure Devopsで動かした場合、実際の実行状況を見ることが出来ないので、画面キャプチャは重要な情報です。結論から言うとAzure DevOpsの設定は特に必要なくローカルでしかるべき設定をしておけば、それだけでエラーの場合に画面キャプチャを見ることが出来ます。

Seleniumで画面キャプチャを生成する方法

先ず、Seleniumで実行している画面のキャプチャは、Seleniumの機能で撮ることが出来ます。RemoteWebDriverにGetScreenshotというメソッドがあるので、この戻り値のclassのメソッドでSaveAsFileにフルパスのファイル名を指定すれば任意の場所にその画面キャプチャが生成出来ます。

var screenshot = Driver.GetScreenshot();
screenshot.SaveAsFile(@"C:\temp\Capture.jpg");

画面キャプチャを欲しいタイミングに限定する方法

この画面キャプチャは全テストで欲しいわけでありません。成功したテストの場合はいらないので、限定するにはテストの結果が分かる必要があります。これはTestContextを使えば取得できます。TestCleanupで、TestContextのCurrentTestOutcomeプロパティを見ればテストの結果も分かるし、またTestNameプロパティでテスト名も取れるので、この名前をファイル名にすれば自動的にユニークに出来ます。ただMSTestでTestContextを使う場合ちょっと癖があります。

TestContextの設定

TestContextのインスタンスをプロパティで1個、staticフィールドで1個、合計2つ持つ必要があります。そして、ClassInitializeで、staticフィールドのTestContextにコピーして、実際に使う時は何も設定しないプロパティのTestContextを使います。プロパティのTestContextを使わずにTestCleanupで_testContextをチェックするとこのテストは何個回していても全て同じテスト名が来てしまいます。何故か何も設定していないプロパティのテスト名は更新されています。staticプロパティ1個だけにすると今度はテストが実行できなくなります。stackoverflowにも書いてありましたが、この2個持ち以外の方法ではうまく行きませんでした。。。

public TestContext TestContext { get; set; }
private static TestContext _testContext;

[ClassInitialize]
public static void ClassInitialize(TestContext testContext)
{
    _testContext = testContext;
    //TestContext = testContext;    //x:フィールドには入れずにプロパティだけに入れると何故かテストが実行できない

[TestCleanup]
public void TestCelean()
{
    _testContext.CurrentTestOutcome //x:本来こっちで良い気がするが、このインスタンスは最初のテスト情報しか入らない
    TestContext.CurrentTestOutcome  //〇:本来こっちには何も入れてない気がするが、このインスタンスはその時動いたテスト情報が入る

テストが成功しなかった時に画面キャプチャを添付する方法

さて、TestContextでテストの結果が取れるようになったので、あとはTestCleanupで、_testContext.CurrentTestOutcomeが成功の場合はreturnしてやります。成功「以外」の場合とすることで、タイムアウトや例外の場合なども対象となります。この状態でTestContextにAddResultFileすればそのファイルを関連画像として扱う事が出来ます。

[TestCleanup]
public void TestCelean()
{
    if (TestContext.CurrentTestOutcome == UnitTestOutcome.Passed) return;
    var screenshot = Driver.GetScreenshot();
    var screenshotFile = Path.Combine(Directory.GetCurrentDirectory(), $"{TestContext.TestName}.png");
    screenshot.SaveAsFile(screenshotFile);
    TestContext.AddResultFile(screenshotFile);
}

これで実際にテストが失敗した時にテストエクスプローラーでみるとこんな感じになります。
image.png
image.png

Azure DevOpsでエラーの場合に画面キャプチャを見る

この設定をしておけば、Azure DevOpsでテストタスクを実行するだけで、自動的にエラーの場合に画面キャプチャを見ることが出来ます!実際にPipelineで画像をみたい時は、Pipelineの結果画面で、1.Testsをクリック。実際にエラーになったテストが表示されるので、2.テスト名をクリック、3.Attachmentsをクリックすると4にエラーの場合の画面キャプチャが表示されます。便利!
image.png

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

DbContextOptionsBuilderに”UseSqlite”が含まれていませんと言われた時の対処法

ASP.NET Coreのチュートリアルをこなしている最中に以下のようなコンパイルエラーが
発生した。

’DbContextOptionsBuilder' does not contain a definition for 'UseSqlite' and no accessible extension method 'UseSqlite' accepting a first argument of type 'DbContextOptionsBuilder' could be found (are you missing a using directive or an assembly reference?)

どうやら、'UseSqlite'とやらが’DbContextOptionsBuilder'に含まれていないとのが原因らしい。

対処法

対処方法は単純で、含まれていないのなら、インストールすれば良いとのことで、
ターミナルを開いて、以下のコマンドを実行するれなよい。

    dotnet add package Microsoft.EntityFrameworkCore.Sqlite

この状態でコンパイルをかけると問題なくコンパイルに成功した。

動作環境

 OS : macOS Ver.10.15.17
 エディタ:VScode

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

[C#][WPF]ライフゲームを作ってみるver.1

自分の勉強の為と興味本位でC#(WPF)でライフゲームを作成してみます。

なぜC#で、かつWPFで作るのかと問われると特に意味はありません。何かの縛りプレイだと思っていただくほかないです。
最初はMVVMを守って作ろうかと思ってましたが、DataGridのバインディングがクセ強かったのでとりあえず全部コードビハインドに書いてまずは動かしていきます。

ライフゲームとは

セル・オートマトンの一種
 以下のルールに従って、生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲーム

  • 誕生:死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。

  • 生存:生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。

  • 過疎:生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。

  • 過密:生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

ライフゲーム by Wikipedia

WPFで作るにあたって

使用するコントロールはDataGridで、各セルを黒(生存)と白(死滅)で塗り分けていきます。

MainWindow.xaml
<Window x:Class="WpfTestView.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" ResizeMode="NoResize"
        Title="MainWindow" Height="650" Width="600" >
    <Grid Name="parentGridSample">
        <StackPanel>
            <Grid Name="gridSample">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Button Grid.Column="0" Content="Refresh" Click="RefreshButton_Click" />
                <Button Grid.Column="1" Content="Random" Click="RandomButton_Click"/>
                <Button Grid.Column="2" Content="Start" Click="StartButton_Click"/>
            </Grid>
            <DataGrid Name="dgSample" HeadersVisibility="None" IsReadOnly="True"
                      SelectionMode="Single" SelectionUnit="Cell"
                      MouseUp="dgSample_MouseUp" SizeChanged="dgSample_SizeChanged">
                <DataGrid.Columns>
                    <DataGridTextColumn>
                        <DataGridTextColumn.CellStyle>
                            <Style TargetType="DataGridCell">
                                <Setter Property="Background" Value="White"/>
                            </Style>
                        </DataGridTextColumn.CellStyle>
                    </DataGridTextColumn>
                </DataGrid.Columns>
            </DataGrid>
        </StackPanel>
    </Grid>
</Window>

全てイベントドリブンとして実装、コードビハインドでコントロールを操作しているので全く持ってWinFormsと違いありません。
これが上手く動けばMVVMにリファクタリングしていく予定です。

MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace WpfTestView
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainView : Window
    {
        // 内部的なデータテーブル
        private DataTable _dataTable = new DataTable();
        // 次元
        private readonly int _rank = 50;
        // 前世代
        private Dictionary<int, List<bool>> preGeneration = new Dictionary<int, List<bool>>();
        // 現世代
        private Dictionary<int, List<bool>> curGeneration = new Dictionary<int, List<bool>>();

        public MainView()
        {
            InitializeComponent();

            InitializeDataGrid();
            InitializeGeneration();
        }

        private void InitializeDataGrid()
        {
            dgSample.Columns.Clear();
            _dataTable.Rows.Clear();
            _dataTable.Columns.Clear();

            for (int i = 0; i < _rank; i++)
            {
                _dataTable.Columns.Add();
                _dataTable.Rows.Add();
            }
            dgSample.ItemsSource = new DataView(_dataTable);

            ResizeDataGrid();
        }

        private void InitializeGeneration()
        {
            preGeneration.Clear();
            curGeneration.Clear();
            preGeneration = Enumerable.Range(0, _rank).ToDictionary(_ => _, _ => Enumerable.Repeat(false, _rank).ToList());
            curGeneration = Enumerable.Range(0, _rank).ToDictionary(_ => _, _ => Enumerable.Repeat(false, _rank).ToList());
        }

        private void ResizeDataGrid()
        {
            if (_dataTable.Columns.Count == 0) return;

            double parentHeghit = parentGridSample.ActualHeight;
            double parentWidth = parentGridSample.ActualWidth;
            double buttonHeight = gridSample.ActualHeight;

            // ボタンを画面上部に付けているのでその分を全体から引いて、行数で均等に割る
            dgSample.MinRowHeight = 0;
            dgSample.RowHeight = (parentHeghit - buttonHeight) / _dataTable.Rows.Count;
            // 何故か親ウィンドウの幅をそのまま割ると少しズレるので-2というマジックナンバーをつけている
            foreach (var col in dgSample.Columns)
            {
                col.MinWidth = 0;
                col.Width = (parentWidth - 2) / _dataTable.Columns.Count;
            }
        }

        private void dgSample_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            Trace.WriteLine(MethodBase.GetCurrentMethod().Name);

            ResizeDataGrid();
        }

        private void RefreshButton_Click(object sender, RoutedEventArgs e)
        {
            Trace.WriteLine(MethodBase.GetCurrentMethod().Name);

            InitializeDataGrid();
            InitializeGeneration();
        }

        private void dgSample_MouseUp(object sender, MouseButtonEventArgs e)
        {
            Trace.WriteLine(MethodBase.GetCurrentMethod().Name);

            // マウスクリックされたセルを取得して色を反転させる
            var curCellInfo = dgSample.CurrentCell;
            var curCell = curCellInfo.Column.GetCellContent(curCellInfo.Item).Parent as DataGridCell;
            if (curCell == null) return;

            int rowIdx = dgSample.Items.IndexOf(curCellInfo.Item);
            int colIdx = curCellInfo.Column.DisplayIndex;
            curGeneration[rowIdx][colIdx] = !curGeneration[rowIdx][colIdx];

            ToggleCellColor(curCell);
        }

        private void StartButton_Click(object sender, RoutedEventArgs e)
        {
            Trace.WriteLine(MethodBase.GetCurrentMethod().Name);

            // とりあえず100世代ライフゲームを実行する
            int gen = 0;
            Task.Factory.StartNew(() =>
            {
                do
                {
                    UpdateGeneration();
                    System.Threading.Thread.Sleep(200);

                    if (curGeneration.All(_n => _n.Value.All(_ => !_))) break;
                } while (++gen < 100);
            });
        }

        private void UpdateGeneration()
        {
            for (int i = 0; i < _rank; i++)
            {
                for (int j = 0; j < _rank; j++)
                {
                    // データのコピー
                    preGeneration[i][j] = curGeneration[i][j];
                }
            }


            for (int i = 0; i < _rank; i++)
            {
                for (int j = 0; j < _rank; j++)
                {
                    int above = i == 0 ? _rank - 1 : i - 1;
                    int below = i == _rank - 1 ? 0 : i + 1;
                    int left = j == 0 ? _rank - 1 : j - 1;
                    int right = j == _rank - 1 ? 0 : j + 1;

                    int aliveCell = 0;
                    if (preGeneration[above][left]) aliveCell++;
                    if (preGeneration[above][j]) aliveCell++;
                    if (preGeneration[above][right]) aliveCell++;
                    if (preGeneration[i][left]) aliveCell++;

                    if (preGeneration[i][right]) aliveCell++;
                    if (preGeneration[below][left]) aliveCell++;
                    if (preGeneration[below][j]) aliveCell++;
                    if (preGeneration[below][right]) aliveCell++;

                    if (!preGeneration[i][j])
                    {
                        if (aliveCell == 3)
                        {
                            // 誕生
                            curGeneration[i][j] = true;
                        }
                    }
                    else if (aliveCell <= 1)
                    {
                        // 過疎
                        curGeneration[i][j] = false;
                    }
                    else if (aliveCell <= 3)
                    {
                        // 生存
                        curGeneration[i][j] = true;
                    }
                    else if (aliveCell >= 4)
                    {
                        // 過密
                        curGeneration[i][j] = false;
                    }
                }
            }

            for (int i = 0; i < _rank; i++)
            {
                for (int j = 0; j < _rank; j++)
                {
                    // 前世と現世で状態が変われば反転させる
                    if (preGeneration[i][j] != curGeneration[i][j])
                    {
                        if (Application.Current.Dispatcher.CheckAccess())
                        {
                            ToggleCellColor(GetCell(i, j));
                        }
                        else
                        {
                            Application.Current.Dispatcher.Invoke(() =>
                            {
                                ToggleCellColor(GetCell(i, j));
                            });
                        }
                    }
                }
            }
        }

        private void RandomButton_Click(object sender, RoutedEventArgs e)
        {
            Trace.WriteLine(MethodBase.GetCurrentMethod().Name);

            try
            {
                InitializeGeneration();

                // 全体の1/3のセルを生存状態にする
                int totalCellCount = _rank * _rank;
                var randomList = GetRandomRange(0, totalCellCount, totalCellCount / 3);
                for (int i = 0; i < _rank; i++)
                {
                    for (int j = 0; j < _rank; j++)
                    {
                        DataGridCell cell = GetCell(i, j);
                        SolidColorBrush brush = cell.Background as SolidColorBrush;
                        if (randomList.Any(_r => _r / _rank == i && _r % _rank == j))
                        {
                            // 黒
                            curGeneration[i][j] = true;
                            if (brush != Brushes.Black) cell.Background = Brushes.Black;
                        }
                        else
                        {
                            // 白
                            curGeneration[i][j] = false;
                            if (brush == Brushes.Black) cell.Background = Brushes.White;
                        }

                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), "Exception Occurred !!");
            }
        }

        private List<int> GetRandomRange(int min, int max, int count)
        {
            if (min > max || count <= 0) return null;

            Random random = new Random(DateTime.Now.Millisecond);
            List<int> list = new List<int>();

            while (list.Count < count)
            {
                int r = random.Next(min, max);
                if (list.Contains(r)) continue;

                list.Add(r);
            }
            return list;
        }

        private DataGridCell GetCell(int i, int j)
        {
            DataGridRow row = dgSample.ItemContainerGenerator.ContainerFromIndex(i) as DataGridRow;
            DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(row);
            return presenter.ItemContainerGenerator.ContainerFromIndex(j) as DataGridCell;
        }

        private T GetVisualChild<T> (Visual parent) where T : Visual
        {
            T child = default(T);
            int visCt = VisualTreeHelper.GetChildrenCount(parent);
            for (int i = 0; i < visCt; i++)
            {
                Visual v = VisualTreeHelper.GetChild(parent, i) as Visual;
                child = v as T;
                if (child == null) child = GetVisualChild<T>(v);

                if (child != null) break;
            }
            return child;
        }

        private void ToggleCellColor(DataGridCell cell)
        {
            SolidColorBrush curCellColor = cell.Background as SolidColorBrush;
            if (curCellColor == Brushes.Black)
            {
                cell.Background = Brushes.White;
            }
            else
            {
                cell.Background = Brushes.Black;
            }
        }
    }
}

ちなみに途中記載している行数・列数からセルを取得するメソッドについては、簡便化の為エラー処理は一切していない。
もし実際に書く場合にはもう少し丁寧に書いた方がいいです。

上記コードで実際に動かしてみたのがこちら
LaifeGame1.gif

今回はもう開き直って全部xaml.csに書きましたが、性分的にはxaml.csには1行も書きたくない派なので
次回、MVVMパターンに則って書き直しを行っていきます。

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

動的に変化するXMLファイルをLinQで読み込む【C#】

タイトルの件の記事は既出とは思うが、
投稿の練習&後輩の教育用にローカライズされたものが欲しかったので。

プロジェクト作成(本題と関係ないので読み飛ばしてね)

新しいプロジェクトの作成でWindowsフォームアプリケーション(.NET Framework)を選択
image.png
適当に名前つけて作成
image.png
とりあえずテンプレートを用意。
ここからボタンとテキストボックスを追加してXMLを読み込む処理を入れていきます。
image.png

ボタンとテキストボックス、あとイベント追加(本題と関係ないので読み飛ばしてね その2)

コントロール(ボタンとテキストボックス)の追加
それぞれツールボックスからドラッグ&ドロップで追加できる。
image.png
テキストボックスは複数行表示させたいので、
プロパティ > プロパティ > Multiline = True に設定
image.png
そしていい感じにテキストボックスを大きくする。
image.png
ボタンの
プロパティ > イベント > Click の所をダブルクリックして、
クリックイベントを追加する。
image.png
image.png
コードが自動生成される。
image.png


ここから本題:XML読み込みをLINQで実装する

1. XMLファイルの作成

まずは読み込むためのXMLファイルを作成する。
ひな形はこんな感じ

Data.xml
<?xml version="1.0" encoding="utf-8" ?>

別にメモ帳でも秀丸でも書ければ何でもいいけど、
visualStudioで書くなら
ソリューションエクスプローラー > 何もないところで右クリック > 追加 > 新しい項目
(またはCtrl + Shift + A でも良い)
image.png
新しい項目の追加 ウィンドウが開くので XMLファイル を選択し、
適当な名前つけて追加する
image.png
XMLファイルが自動生成される。
まずはここに読み込ませたいデータを書いていきます。
image.png
今回読み込みたいXMLのデータはこんな感じ

Data.xml
<?xml version="1.0" encoding="utf-8" ?>
<Pokemon>
    <Gosannke>
        <PokeName>Fushigidane</PokeName>
        <PokeName>Hitokage</PokeName>
        <PokeName>Zenigame</PokeName>
    </Gosannke>
    <Fushigidane>
        <Type>kusa</Type>
        <Type>doku</Type>
        <Waza>Taiatari</Waza>
        <Waza>Nakigoe</Waza>
        <Waza>Turunomuti</Waza>
    </Fushigidane>
    <Hitokage>
        <Type>honoo</Type>
        <Waza>Hikkaku</Waza>
        <Waza>Nakigoe</Waza>
    </Hitokage>
    <Zenigame>
        <Type>mizu</Type>
        <Waza>Taiatari</Waza>
        <Waza>Shippowohuru</Waza>
        <Waza>Mizudeppo</Waza>
        <Waza>Karanikomoru</Waza>
    </Zenigame>
</Pokemon>

表にするとこう

ポケモン名 タイプ わざ
フシギダネ くさ / どく たいあたり / なきごえ / つるのムチ
ヒトカゲ ほのお ひっかく / なきごえ
ゼニガメ みず たいあたり / しっぽをふる / みずでっぽう / からにこもる

見てわかる通り、タイプとわざの数にばらつきがあります。
(最終的にこういうデータを扱いたいという話)

とりあえず手始めに、以下のデータを読み込む処理をLINQで書いてみます。

Data.xml
<?xml version="1.0" encoding="utf-8" ?>
<Pokemon>
    <Gosannke>
        <PokeName>Fushigidane</PokeName>
        <PokeName>Hitokage</PokeName>
        <PokeName>Zenigame</PokeName>
    </Gosannke>
</Pokemon>

2. XMLを読み込む

usingを2つ追加し、パスを取得して読み込んでみます。

Form.cs
using System;
using System.Windows.Forms;
using System.IO;
using System.Data;
using System.Linq;                   // 追加したusing
using System.Xml.Linq;               // 追加したusing
using System.Collections.Generic; 
private void Button1_Click(object sender, EventArgs e)
{
    // カレントディレクトリを2つ上の階層に設定し直す
    Directory.SetCurrentDirectory(@"..\..\");

    // Data.xmlのパスを取得
    string xmlFilePath = Directory.GetCurrentDirectory() + @"\Data.xml";

    // Data.xmlを読み込む
    XElement pokeData = XElement.Load(xmlFilePath);

    // 御三家ポケモンの名前を全て取得
    IEnumerable<string> GosannkeInfo = 
        from poke in pokeData.Elements("Gosannke").Elements("PokeName") select poke.Value;

    // 取得したポケモンの名前をテキストボックスに出力
    foreach (string pokeName in GosannkeInfo)
    {
        textBox1.Text += pokeName + "\r\n";
    }

    // カレントディレクトリを元の場所に戻す
    Directory.SetCurrentDirectory(@".\bin\Debug");
}

実行してボタンを押すとこんな感じ。
ポケモンの名前を取得できました。
image.png

3. 【応用】取得したタグ名の中身を読みこむ

では本命のこのxmlを読み込んでみましょう

Data.xml
<?xml version="1.0" encoding="utf-8" ?>
<Pokemon>
    <Gosannke>
        <PokeName>Fushigidane</PokeName>
        <PokeName>Hitokage</PokeName>
        <PokeName>Zenigame</PokeName>
    </Gosannke>
    <Fushigidane>
        <Type>kusa</Type>
        <Type>doku</Type>
        <Waza>Taiatari</Waza>
        <Waza>Nakigoe</Waza>
        <Waza>Turunomuti</Waza>
    </Fushigidane>
    <Hitokage>
        <Type>honoo</Type>
        <Waza>Hikkaku</Waza>
        <Waza>Nakigoe</Waza>
    </Hitokage>
    <Zenigame>
        <Type>mizu</Type>
        <Waza>Taiatari</Waza>
        <Waza>Shippowohuru</Waza>
        <Waza>Mizudeppo</Waza>
        <Waza>Karanikomoru</Waza>
    </Zenigame>
</Pokemon>

先ほどの foreach の箇所を以下のように書き換えました。
foreach がネストしてて冗長に見えますね…。

Form.cs
// 取得したポケモンの名前をテキストボックスに出力
foreach (string pokeName in PokeInfo)
{
    textBox1.Text += pokeName + "\r\n";

    // <Type>要素の取得
    IEnumerable<string> TypeInfo = from type in pokeData.Elements(pokeName).Elements("Type") select type.Value;
    textBox1.Text += "TYPE : " + "\r\n";
    foreach (string Type in TypeInfo)
    {
        textBox1.Text += " " + Type + "\r\n";
    }

    // <Waza>要素の取得
    IEnumerable<string> WazaInfo = from waza in pokeData.Elements(pokeName).Elements("Waza") select waza.Value;
    textBox1.Text += "WAZA : " + "\r\n";
    foreach (string Waza in WazaInfo)
    {
        textBox1.Text += " " + Waza + "\r\n";
    }

    // 見栄えのための改行
    textBox1.Text += "\r\n";
}

一応動作自体は期待通り。
でもコードが気持ち悪いのでforeach 部分をLINQで書き直してみましょう。
image.png

4. foreach文をLINQにリファクタリングする

foreachのデメリットは、単純にネストが深く可読性が悪くなる点です。
動作を変えず、可読性をよくするために、LINQで書き直します。
分かり易いように、<Type> 要素の部分だけをLINQに書き換えました。

Form.cs
// 取得したポケモンの名前をテキストボックスに出力
foreach (string pokeName in PokeInfo)
{
    textBox1.Text += pokeName + "\r\n";

    // <Type>要素の取得
    IEnumerable<string> TypeInfo = from type in pokeData.Elements(pokeName).Elements("Type") select type.Value;
    textBox1.Text += "TYPE : " + "\r\n";
    TypeInfo.ToList().ForEach(Type => { textBox1.Text += " " + Type + "\r\n"; });

    // ↑と↓を見比べると分かり易い!

    // <Waza>要素の取得
    IEnumerable<string> WazaInfo = from waza in pokeData.Elements(pokeName).Elements("Waza") select waza.Value;
    textBox1.Text += "WAZA : " + "\r\n";
    foreach (string Waza in WazaInfo)
    {
        textBox1.Text += " " + Waza + "\r\n";
    }

    // 見栄えのための改行
    textBox1.Text += "\r\n";
}

<Waza> 要素も同様にLINQで書いてみましょう。

Form.cs
// 取得したポケモンの名前をテキストボックスに出力
foreach (string pokeName in PokeInfo)
{
    textBox1.Text += pokeName + "\r\n";

    // <Type>要素の取得
    IEnumerable<string> TypeInfo = from type in pokeData.Elements(pokeName).Elements("Type") select type.Value;
    textBox1.Text += "TYPE : " + "\r\n";
    TypeInfo.ToList().ForEach(Type => { textBox1.Text += " " + Type + "\r\n"; });

    // <Waza>要素の取得
    IEnumerable<string> WazaInfo = from waza in pokeData.Elements(pokeName).Elements("Waza") select waza.Value;
    textBox1.Text += "WAZA : " + "\r\n";
    WazaInfo.ToList().ForEach(Waza => { textBox1.Text += " " + Waza + "\r\n"; });

    // 見栄えのための改行
    textBox1.Text += "\r\n";
}

タグ要素の取得は、共通の処理なので関数化しましょう。

Form.cs
private void OutputPokeData(XElement pokeData, string pokeName, string tag)
{
    IEnumerable<string> Infos = from Info in pokeData.Elements(pokeName).Elements(tag) select Info.Value;
    textBox1.Text += tag + " : " + "\r\n";
    Infos.ToList().ForEach(Info => { textBox1.Text += " " + Info + "\r\n"; });
}
Form.cs
// 取得したポケモンの名前をテキストボックスに出力
foreach (string pokeName in PokeInfo)
{
    textBox1.Text += pokeName + "\r\n";

    // <Type>要素の取得
    OutputPokeData(pokeData, pokeName, "Type");

    // <Waza>要素の取得
    OutputPokeData(pokeData, pokeName, "Waza");

    // 見栄えのための改行
    textBox1.Text += "\r\n";
}

とっても見やすくなりましたね!
最後にこのforeachもLINQに書き直して、ソース全体を見てみましょう。

Form.cs
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Windows.Forms;
using System.Xml.Linq;
using System.IO;

namespace XMLReader
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Button1_Click(object sender, EventArgs e)
        {
            // カレントディレクトリを2つ上の階層に設定し直す
            Directory.SetCurrentDirectory(@"..\..\");

            // Data.xmlのパスを取得
            string xmlFilePath = Directory.GetCurrentDirectory() + @"\Data.xml";

            // Data.xmlを読み込む
            XElement pokeData = XElement.Load(xmlFilePath);

            // 御三家ポケモンの名前を全て取得
            IEnumerable<string> PokeInfo = 
                from poke in pokeData.Elements("Gosannke").Elements("PokeName") select poke.Value;

            // 取得したポケモンの名前をテキストボックスに出力
            PokeInfo.ToList().ForEach(pokeName => {
                textBox1.Text += pokeName + "\r\n";         // ポケモン名表示
                OutputPokeData(pokeData, pokeName, "Type"); // <Type>要素の取得
                OutputPokeData(pokeData, pokeName, "Waza"); // <Waza>要素の取得
                textBox1.Text += "\r\n";                    // 見栄えのための改行
            });

            // カレントディレクトリを元の場所に戻す
            Directory.SetCurrentDirectory(@".\bin\Debug");
        }

        private void OutputPokeData(XElement pokeData, string pokeName, string tag)
        {
            IEnumerable<string> Infos = from Info in pokeData.Elements(pokeName).Elements(tag) select Info.Value;
            textBox1.Text += tag + " : " + "\r\n";
            Infos.ToList().ForEach(Info => { textBox1.Text += " " + Info + "\r\n"; });
        }
    }
}

動作も全く同じであることが確認できました。
image.png

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

Azure DevOpsのPipelineのtaskでMSTestを動かす方法

結論

PipelineのtaskでVSTestは使わず、DotNetCoreCLIのtestを使うこと

Azure DevOpsのテストの設定にはまる

Azure DevOpsのPipelineでMSTestを動かしたいと思いました。以前ユニットテストをAzure DevOpsにのせた時は特に苦労した覚えもなかったので(以前試したのは.NET FrameworkでAzure DevOpsもyamlではなくGUIで設定していました)、今回も特に苦労することは無いだろうと思っていました。ところがいざPipelineで動かそうと思ったら設定にめちゃめちゃはまりました。結局結論で解決出来たのですが、それまでに結構時間を取られたのでその行程を書いておきます。

Pipelineで動かす具体例

プロジェクトを作って、以下のようなテストを作ります。

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTestProject1
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {
            int a = 1;
            Assert.AreEqual(a, 1);
        }

        [TestMethod]
        public void TestMethod2()
        {
            int a = 2;
            Assert.AreEqual(a, 1);
        }
    }
}

ローカルでVisual Studioのテストエクスプローラーで動かせばTestMethod2は当然失敗します。
image.png

ところがこのプロジェクトをAzure DevOpesのPipelineのTaskにVSTestを作って動かした場合、このタスクは成功します!?

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

image.png

VSTestの設定を変えてみる

つまり、VSTestのデフォルトの設定ではテスト結果に連動して成功、失敗を返さない状態です。なんでデフォルトがこの状態になっているのか謎です。しょうがないので設定を変えてみました。先ずSelect tests usingという項目があって、Test assembliesになっていたので、Test runを選択してみました。runなら実行するだろうと。
image.png

これで無事タスクは失敗になりました。
image.png

ところがです。実はこれテストが成功の状態でも失敗になってしまいます。つまりただの設定ミスです。

エラーを調べてみる

##[error]Input validation failed with exception: Invalid on demand test run id 0. Has to be greater than 0.
##[error]Error: The process 'D:\a\_tasks\VSTest_ef087383-ee5e-42c7-9a53-ab56c98420f9\2.170.1\Modules\DTAExecutionHost.exe' 

エラーをググってみるとstackoverflowGitHubのissuesMSの公式ドキュメントなど色々情報は出ます。設定が悪い、バージョンが悪いパスが悪いVisual Studio Test Platform Installerを入れろなど。調べた感じ掲示板で質問だけ投げていて答えが返ってない例や、色々話したけれど結局解決していない例なども出るのでみんなはまっているけれど解決出来ていないのかな?と思います。これ以外にもVSTestの設定を色々変えて、何十回もトライ&エラーしましたが、結局テスト結果と連動してVSTestが動くように出来ませんでした。(むしろこの解決方法は誰か教えて下さい)

Azure DevOpesのtaskを変えてみる

で、MSTestをテスト結果と連動して動かす方法ですが、VSTestを使わないことで解決しました。具体的には.NET Coreのtaskでtestを選ぶ。これだけです。

- task: DotNetCoreCLI@2
  inputs:
    command: 'test'

これで、何の設定を加えなくてもちゃんと実際のテスト結果と連動して動く状態になりました。

成功した場合
image.png
image.png

失敗した場合
image.png
image.png

まとめ

MSTestを動かすのであれば、どう考えてもVSTest一択だと思っていたので、設定を色々変えて試した見ましたが、結局はVSTestを使わずDotNetCoreCLIのtestを使うという結論でした。Azure DevopsのデフォルトのyamlでもVSTestが設定されているので、VSTestのデフォルトの設定のままで普通にテスト結果と連動して動くようにして欲しいです。。

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