20190324のC#に関する記事は14件です。

F#超超超入門

F# is 何 ?

Microsoftが開発した.NET上で動く関数型言語です.

.NET互換の環境であれば動作するので, .NET Framework上でも, .NET Core上でも, mono上でも動きます. つまり, Windows/Mac OS/Linuxどこでも動く言語なわけです.

なぜ F# なのか...

@cannorinさんが投稿した「F# を知ってほしい」や, p_tan's blogさんの「なぜC#よりもF#なのか 2017」を読んでいただくことで, F#の魅力をヒシヒシと感じていただくことができるかと思います.

私の拙い語彙力ではあまり魅力を伝えられませんので, 先人の方々のお言葉をお借りして, 魅力を感じていただければと思います.

ということで, 早速 F#に超超超入門 していきましょう.
(とは言っても Hello, World するだけですが... )

開発環境

Windows/Mac OS環境であれば, Visual Studioをインストールすればよいと思います. Linuxの場合は, .NET Core + VS Code の組み合わせが良いのではないでしょうか?そこはお好みでどうぞ.

ちなみに私は, Win/Mac両環境ともVS Code + .NET Core環境でやっていますが, Visual Studioを導入してしまった方が楽です. 今回はWindows環境向けに記事を書いていきますが, Mac OS環境でも大して変わりません. 変わるとしてもIDEの使い方くらいです(それが一番大きい気もするが...).

Visual Studioは以下からCommunity版のダウンロード/インストールをしてください.

Visual Studio ダウンロードページ

基本的にはOKを連打していれば良いのですが, 下のようなウィンドウが表示されたら,
FSharp2.png
【変更】ボタンを押下して,
FSharp1.png
【F#デスクトップ言語のサポート】と【F#言語サポート】にチェックを入れてあげてください.
この手続きを忘れても後からどうとでもなるので, 最悪忘れてしまっても問題ありません.

さあ, あとはVisual Studioがインストールされるのを待つばかりです!!

Hello, World!!

すべてのプログラマはこの挨拶を標準出力に表示せねばなりません. もはやこれは義務なのです.

例に漏れずにF#でHello, Worldを出力させていきましょう.

まずはVisual Studioを起動してみてください. Visual Studio 2017 では次のようなページが出ると思います.
FSharp3.png
もしかすると画面の構成や表示されている内容が違うかもしれませんが, 気にせずに, 【新しいプロジェクトの作成】を押下しましょう.

そうすると以下のような画面が表示されると思いますので, 【Visual F#】を選択 →【コンソール アプリケーション (.NET Framework)】または【コンソール アプリケーション (.NET Core)】を選択 →【OK】を押下します.

FSharp4.png

すると, 自動的に以下のようなコードが生成されると思います.
FSharp5.png

そうしたら, 6行目を

printfn "Hello, World!!"

と書き換えてあげて, 【Ctrl+F5】を押すと...
FSharp6.png

なんと黒い画面に【Hello, World!!】が出力されました.

おめでとうございます. これであなたは, F#プログラマの第一歩を踏み出すことができました.
今日は素晴らしい記念日となるでしょう.

おわりに

茶番にお付き合いいただき, ありがとうございました.

私は普段C#とC++をメインに書いているのですが, F#の素晴らしさを広めるべく活動をせねばと思い立ち, Qiitaに投稿をした次第です.

F#はC#に比べて非常に知名度が低い言語です. しかし, それはC#より劣っていることを意味していません. 一度でもF#に触れてしまうとC#には戻れなくなるほどの魅力を持っています(誇大表現).  

ただこれは, 常にF#がC#より優位, または 常にC#がF#より優位な言語であると極論を言いたいわけではありません. お互いの良いところをコラボレートすることに高い価値があると思います.

幸いなことに, F#とC#は両言語とも.NET環境上で動作するため, 非常に親和性が高いです.
F#が得意なところはF#で, C#が得意なところはC#で書くような流れになってくれればな~と思います.

余談

私は常日頃からMicrosoft Docs/MSDNの日本語訳を解読することが難易度の非常に高い作業だと感じていました.
そのため, 公式ドキュメントをもう少しわかりやすく日本語化したもの+簡単なサンプルコードを追加したサイトを作成しました.

Midoliy.com |> F#

やはり, 日本語のドキュメントが読みにくいと新規で始める人にとっては高い障壁となりますので, その辺りから解消できればと思っています.
まだまだ書き途中かつ拙い文章ですが, もしF#をこれから始めたいと思っている方がいらっしゃいましたら, ぜひご利用ください.

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

【C#】Selenium用のデモサイトを使ってSeleniumを動かしてみた

初めに

Seleniumユーザーコミュニティが用意しているデモサイトを使ってSeleniumのテストコードを動かしてみた。
以下、その記録。

前提条件

  • Windows 10
  • C#
  • Visual Studio 2017

内容

プロジェクトを作る

Visual Studioを起動して「ファイル>新規作成>プロジェクト」と選択して、単体テストプロジェクトを作成する。
無題.png

NuGenパッケージマネージャーを使って下記の5つをプロジェクトに導入する。

  • Selenium.WebDriver
  • Selenium.Chrome.WebDriver
  • NUnit
  • NUnit.COnsole
  • NUnit3TestAdapter

ページオブジェクトを作る

予約フォーム画面

ReserveFormPage.cs
    class ReserveFormPage
    {
     // フォームに値を設定する
        public static void SetForm(IWebDriver driver)
        {
            // フォームを初期化
            driver.FindElement(By.Id("reserve_year")).Clear();
            driver.FindElement(By.Id("reserve_month")).Clear();
            driver.FindElement(By.Id("reserve_day")).Clear();
            driver.FindElement(By.Id("reserve_term")).Clear();
            driver.FindElement(By.Id("headcount")).Clear();
            driver.FindElement(By.Id("guestname")).Clear();

            // 予約日時(翌日)を設定
            DateTime tommorow = DateTime.Now.AddDays(1);
            driver.FindElement(By.Id("reserve_year")).SendKeys(tommorow.Year.ToString());
            driver.FindElement(By.Id("reserve_month")).SendKeys(tommorow.Month.ToString());
            driver.FindElement(By.Id("reserve_day")).SendKeys(tommorow.Day.ToString());

            // 宿泊日数
            driver.FindElement(By.Id("reserve_term")).SendKeys(1.ToString());

            // 宿泊人数
            driver.FindElement(By.Id("headcount")).SendKeys(1.ToString());

            // 朝食
            driver.FindElement(By.Id("breakfast_on")).Click();

            // プラン
            driver.FindElement(By.Id("plan_b")).Click();

            // 代表者氏名
            driver.FindElement(By.Id("guestname")).SendKeys("たろう");
        }

     // 予約日時のフォームに値を設定する
        public static void SetForm_RserveDate(IWebDriver driver, DateTime dt)
        {
            driver.FindElement(By.Id("reserve_year")).Clear();
            driver.FindElement(By.Id("reserve_month")).Clear();
            driver.FindElement(By.Id("reserve_day")).Clear();

            driver.FindElement(By.Id("reserve_year")).SendKeys(dt.Year.ToString());
            driver.FindElement(By.Id("reserve_month")).SendKeys(dt.Month.ToString());
            driver.FindElement(By.Id("reserve_day")).SendKeys(dt.Day.ToString());
        }

     // 予約者のフォームに値を設定する
        public static void SetForm_GuestName(IWebDriver driver,string name)
        {
            driver.FindElement(By.Id("guestname")).Clear();
            driver.FindElement(By.Id("guestname")).SendKeys(name);
        }

     // 次へボタンを押下する
        public static void ClickNextBtn(IWebDriver driver)
        {
            driver.FindElement(By.Id("goto_next")).Click();
        }
    }

予約確認画面

ReserveConfirmPage.cs
    class ReserveConfirmPage
    {
     // 確認ボタンを押下する
        public static void ClickConfirmBtn(IWebDriver driver)
        {
            driver.FindElement(By.Id("commit")).Click();
        }

     // 戻るボタンを押下する
        public static void ClickBackBtn(IWebDriver driver)
        {
            driver.FindElement(By.Id("returnto_index")).Click();
        }

     // 画面に表示されている「予約者名」を取得する
        public static string GetGuestName(IWebDriver driver)
        {
            return driver.FindElement(By.Id("gname")).Text;
        }
    }

予約完了画面

ReserveCompletePage.cs
    class ReserveCompletePage
    {
     // 画面に表示されているメッセージを取得する
        public static string GetMessage(IWebDriver driver)
        {
            return driver.FindElement(By.XPath("/html/body/div[1]/h1")).Text;
        }
    }

エラー画面

ErrorPage.cs
    class ErrorPage
    {
     // 画面のタイトルを取得する
        public static string GetPageTitle(IWebDriver driver)
        {
            return driver.Title;
        }

     // 画面に表示されている詳細エラーメッセージを取得する
        public static string GetErrorDetail(IWebDriver driver)
        {
            return driver.FindElement(By.Id("errorcheck_result")).Text;
        }
    }

テストコードを作る

ReservationTest.cs
    [TestFixture]
    public class ReservationTest
    {
        private  IWebDriver d;

        [SetUp]
        public void SetUp()
        {
            this.d = new ChromeDriver();
            d.Url = "http://example.selenium.jp/reserveApp/";
        }

        [TearDown]
        public void TearDown()
        {
            this.d.Quit();
        }

        /// <summary>
        /// 予約画面で正しい情報を入力して、予約完了まで行けることを確認するテスト
        /// </summary>
        [TestCase()]
        public void BasicReservationTest()
        {
            // 予約入力画面
            ReserveFormPage.SetForm(d);
            ReserveFormPage.ClickNextBtn(d);

            // 予約確認画面
            ReserveConfirmPage.ClickConfirmBtn(d);

            // 予約完了画面
            Assert.AreEqual(ReserveCompletePage.GetMessage(d), "予約を完了しました。");

        }

        /// <summary>
        /// 過去日で予約しようとして、エラー画面に遷移するテスト
        /// </summary>
        [TestCase()]
        public void ReservationForPastDaysTest()
        {
            DateTime past_day = DateTime.Now.AddDays(-1);

            // 予約入力画面
            ReserveFormPage.SetForm(d);
            ReserveFormPage.SetForm_RserveDate(d, past_day);
            ReserveFormPage.ClickNextBtn(d);

            // エラー画面
            Assert.AreEqual(ErrorPage.GetPageTitle(d), "予約エラー");
            Assert.AreEqual(ErrorPage.GetErrorDetail(d), "宿泊日には、翌日以降の日付を指定してください。");

        }


        /// <summary>
        /// 予約情報を入力して確認画面にいって、もう一度入力画面に戻って予約者の名前を変更して、再度確認画面に行って予約を確定するテスト
        /// </summary>
        [TestCase()]
        public void BackwardAndForwardTest()
        {

            // 予約画面操作
            ReserveFormPage.SetForm(d);
            ReserveFormPage.SetForm_GuestName(d, "たろう");
            ReserveFormPage.ClickNextBtn(d);

            // 予約確認画面操作
            ReserveConfirmPage.ClickBackBtn(d);


            // 予約画面操作
            ReserveFormPage.SetForm(d);
            ReserveFormPage.SetForm_GuestName(d,"やまだたろう");
            ReserveFormPage.ClickNextBtn(d);

            // 予約確認画面操作
            ReserveConfirmPage.ClickConfirmBtn(d);

            // 予約完了画面操作
            Assert.AreEqual(ReserveCompletePage.GetMessage(d), "予約を完了しました。");
        }
    }

動かす

Visual Studioから単体テストコードを実行する。
テスト結果がすべてGreenになればOK。

無題2.png

最後に

ページオブジェクトの各種メソッドは、あえてstaticにしてみた。
呼び出す側にコンストラクタを書かなくていい分、テストコード側がすっきりする気がしたので。

以上

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

C# 任意のペイロードを含んだJWT(JSON Web Token)を生成する

このドキュメントの内容

System.IdentityModel.Tokens.Jwt を使って JWT (JSON Web Token) を生成する方法を説明します。
System.IdentityModel.Tokens.Jwt のバージョンは 5.4.0 です。

トークンの生成

トークンプロバイダー

今回定義しているのはトークン生成メソッドのみです。

ITokenProvider
/// <summary>
/// トークンプロバイダー。
/// </summary>
/// <typeparam name="TPayload">ペイロードの型</typeparam>
public interface ITokenProvider<TPayload>
{
    /// <summary>
    /// トークンを生成します。
    /// </summary>
    /// <param name="claim">クレーム</param>
    /// <param name="payload">ペイロード</param>
    /// <param name="expiration">有効期限</param>
    /// <returns>トークン</returns>
    string CreateToken(IClaim claim, TPayload payload, DateTimeOffset expiration);
}

クレーム情報

トークンに与えるクレーム情報のうち、呼び出し元から指定する項目を定義しています。

IClaim
/// <summary>
/// クレーム情報。
/// </summary>
public interface IClaim
{
    /// <summary>
    /// IDを取得します。
    /// </summary>
    string ID { get; }

    /// <summary>
    /// 利用者を取得します。
    /// </summary>
    string Audience { get; }
}

/// <summary>
/// クレーム情報。
/// </summary>
public class MsJwtClaim : IClaim
{
    /// <summary>
    /// IDを取得または設定します。
    /// </summary>
    public string ID { get; set; }

    /// <summary>
    /// 利用者を取得または設定します。
    /// </summary>
    public string Audience { get; set; }
}

トークンプロバイダーの実装

前述の ITokenProvider<TPayload> インターフェースを実装する、System.IdentityModel.Tokens.Jwt を用いたトークンプロバイダーです。
ペイロードは JSON 文字列にして指定します。JSON シリアライザには JSON.net を用いています。

MsJwtProvider
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using Newtonsoft.Json;

/// <summary>
/// トークンプロバイダー。
/// </summary>
/// <typeparam name="TPayload">ペイロードの型</typeparam>
public class MsJwtProvider<TPayload> : ITokenProvider<TPayload>
{
    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="key">RSA秘密鍵</param>
    public MsJwtProvider(RsaSecurityKey key)
    {
        m_Key = key;
    }

    private readonly RsaSecurityKey m_Key;
    private readonly JwtSecurityTokenHandler m_TokenHandler = new JwtSecurityTokenHandler();

    /// <summary>
    /// 発行者を取得または設定します。
    /// </summary>
    public string Issuer { get; set; }

    /// <summary>
    /// トークンを生成します。
    /// </summary>
    /// <param name="claim">クレーム</param>
    /// <param name="payload">ペイロード</param>
    /// <param name="expiration">有効期限</param>
    /// <returns>トークン</returns>
    public string CreateToken(IClaim claim, TPayload payload, DateTimeOffset expiration)
    {
        var credentials = new SigningCredentials(m_Key, "RS256");

        var descriptor = new SecurityTokenDescriptor { };

        descriptor.SigningCredentials = credentials;
        descriptor.Issuer = Issuer;
        descriptor.Audience = claim.Audience;
        descriptor.Expires = expiration.UtcDateTime;
        descriptor.NotBefore = DateTime.UtcNow.AddSeconds(-5);
        descriptor.IssuedAt = DateTime.UtcNow;

        // 定義済でない項目をクレームに含めるには、Claims ではなく Subject に格納します。

        //descriptor.Claims = new Dictionary<string, object> {
        //    { "userpayload", JsonConvert.SerializeObject(payload) }
        //    { "jti", claim.ID }
        //};
        descriptor.Subject = new ClaimsIdentity(new Claim[] {
                new Claim("userpayload", JsonConvert.SerializeObject(payload)),
                new Claim("jti", claim.ID)
            });

        var token = m_TokenHandler.CreateJwtSecurityToken(descriptor);
        var tokenString = m_TokenHandler.WriteToken(token);

        return tokenString;
    }
}

トークンの検証

トークンバリデーター

今回定義しているのはトークン文字列検証メソッドのみです。
検証すると同時に、戻り値としてトークンから取り出したクレーム情報とペイロードを返します。

ITokenValidator
/// <summary>
/// トークンバリデーター。
/// </summary>
/// <typeparam name="TPayload">ペイロードの型</typeparam>
public interface ITokenValidator<TPayload>
{
    /// <summary>
    /// 指定されたトークンを検証します。
    /// </summary>
    /// <param name="token">トークン文字列</param>
    /// <param name="claim">トークンから取り出したクレーム情報</param>
    /// <param name="payload">トークンから取り出したペイロード</param>
    /// <param name="tokenState">トークンの状態</param>
    /// <param name="errorMessage">エラーメッセージ</param>
    /// <returns>トークンが妥当であるかどうか</returns>
    bool ValidateToken(string token
        , out IClaim claim
        , out TPayload payload
        , out TokenState tokenState
        , out string errorMessage
    );
}

トークンの状態

検証したトークンの状態を表す列挙体です。

/// <summary>
/// トークンの状態。
/// </summary>
public enum TokenState
{
    /// <summary>
    /// 不明
    /// </summary>
    Unknown = 0,

    /// <summary>
    /// 妥当
    /// </summary>
    Valid,

    /// <summary>
    /// 不正
    /// </summary>
    Invalid,

    /// <summary>
    /// まだ有効でない
    /// </summary>
    NotBefore,

    /// <summary>
    /// 有効期限切れ
    /// </summary>
    Expired,
}

トークンバリデーターの実装

前述の ITokenValidator<TPayload> インターフェースを実装する、System.IdentityModel.Tokens.Jwt を用いたトークンバリデーターです。
ペイロードは JSON 文字列から復元します。JSON シリアライザには JSON.net を用いています。

MsJwtValidator
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using Newtonsoft.Json;

/// <summary>
/// トークンバリデーター。
/// </summary>
/// <typeparam name="TPayload">ペイロードの型</typeparam>
public class MsJwtValidator<TPayload> : ITokenValidator<TPayload>
{
    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="key">RSA公開鍵</param>
    internal MsJwtValidator(RsaSecurityKey key)
    {
        m_Key = key;

        m_LifetimeValidator = new LifetimeValidator((nbf, exp, token, parameter) =>
        {
            return ValidateLifetime(nbf, exp, out TokenState state, out string message);
        }
        );
    }

    private readonly RsaSecurityKey m_Key;
    private readonly JwtSecurityTokenHandler m_TokenHandler = new JwtSecurityTokenHandler();
    private readonly LifetimeValidator m_LifetimeValidator;

    /// <summary>
    /// 妥当と見なす発行者を取得または設定します。
    /// </summary>
    public string[] ValidIssuers { get; set; }

    /// <summary>
    /// 妥当と見なす利用者を取得または設定します。
    /// </summary>
    public string[] ValidAudiences { get; set; }

    /// <summary>
    /// 指定されたトークンを検証します。
    /// </summary>
    /// <param name="token">トークン文字列</param>
    /// <param name="claim">トークンから取り出したクレーム情報</param>
    /// <param name="payload">トークンから取り出したペイロード</param>
    /// <param name="tokenState">トークンの状態</param>
    /// <param name="errorMessage">エラーメッセージ</param>
    /// <returns>トークンが妥当であるかどうか</returns>
    public bool ValidateToken(string tokenString
        , out IClaim claim
        , out TPayload payload
        , out TokenState tokenState
        , out string errorMessage
    )
    {

        TokenValidationParameters parameters = new TokenValidationParameters();
        parameters.IssuerSigningKey = m_Key;
        parameters.ValidateIssuerSigningKey = true;

        // 発行者を検証するかどうか
        if (ValidIssuers != null && ValidIssuers.Length > 0)
        {
            if (ValidIssuers.Length == 1) { parameters.ValidIssuer = ValidIssuers.First(); }
            else { parameters.ValidIssuers = ValidIssuers; }
            parameters.ValidateIssuer = true;
        }
        else
        {
            parameters.ValidateIssuer = false;
        }

        // 利用者を検証するかどうか
        if (ValidAudiences != null && ValidAudiences.Length > 0)
        {
            if (ValidAudiences.Length == 1) { parameters.ValidAudience = ValidAudiences.First(); }
            else { parameters.ValidAudiences = ValidAudiences; }
            parameters.ValidateAudience = true;
        }
        else
        {
            parameters.ValidateAudience = false;
        }

        // 有効期限を検証するかどうか
        parameters.ValidateLifetime = true;
        parameters.LifetimeValidator = m_LifetimeValidator;

        try
        {

            ClaimsPrincipal claims = m_TokenHandler.ValidateToken(tokenString, parameters, out SecurityToken token);

            // クレーム情報
            claim = new MsJwtClaim()
            {
                ID = GetClaim(claims, "jti),
                Audience = GetClaim(claims, "aud")
            };

            // ペイロード
            string payloadJson = claims.FindFirst("userpayload")?.Value;

            if (string.IsNullOrEmpty(payloadJson))
            {
                payload = default(TPayload);
            }
            else
            {
                payload = JsonConvert.DeserializeObject<TPayload>(payloadJson);
            }

            tokenState = TokenState.Valid;
            errorMessage = null;
            return true;

        }
        catch (SecurityTokenInvalidLifetimeException ex)
        {
            // 有効期限が不正
            claim = null;
            payload = default(TPayload);
            if (ValidateLifetime(ex.NotBefore, ex.Expires, out tokenState, out errorMessage))
            {
                // NotBefore でも Expires でもない
                tokenState = TokenState.Invalid;
                errorMessage = "The token is invalid.";
            }
            return false;
        }
        catch (SecurityTokenInvalidIssuerException)
        {
            // 発行者が不正
            claim = null;
            payload = default(TPayload);
            tokenState = TokenState.Invalid;
            errorMessage = "The issuer is invalid.";
            return false;
        }
        catch (SecurityTokenInvalidAudienceException)
        {
            // 利用者が不正
            claim = null;
            payload = default(TPayload);
            tokenState = TokenState.Invalid;
            errorMessage = "The audience is invalid.";
            return false;
        }
    }

    /// <summary>
    /// 指定されたクレームの値を取得します。
    /// </summary>
    /// <param name="claims">クレーム情報</param>
    /// <param name="key">キー</param>
    /// <returns></returns>
    private string GetClaim(ClaimsPrincipal claims, string key)
    {
        foreach (Claim claim in claims.Claims)
        {
            if (string.Compare(claim.Type, key, true) == 0)
            {
                return claim.Value;
            }
        }
        return null;
    }

    /// <summary>
    /// 有効期限を検証します。
    /// </summary>
    /// <param name="nbf">トークンが有効になる日時</param>
    /// <param name="exp">トークンの有効期限</param>
    /// <param name="state">トークンの状態</param>
    /// <param name="message">エラーメッセージ</param>
    /// <returns>妥当であるかどうか</returns>
    private bool ValidateLifetime(DateTime? nbf, DateTime? exp, out TokenState state, out string message)
    {
        DateTime now = DateTime.Now.ToUniversalTime();

        if (exp.HasValue && exp.Value < now)
        {
            state = TokenState.Expired;
            message = "The token is expired.";
            return false;
        }

        if (nbf.HasValue && nbf.Value > now)
        {
            state = TokenState.NotBefore;
            message = "The token is not yet valid.";
            return false;
        }

        state = TokenState.Valid;
        message = null;
        return true;
    }
}

トークンの検証時に得られる内容

ClaimsPrincipal claims = m_TokenHandler.ValidateToken(tokenString, parameters, out SecurityToken token);

上記のトークン検証メソッドで得られる内容を簡単に説明します。

戻り値 claims の Claims プロパティにはクレームがコレクションとして格納されています。トークン生成時に Subjects として与えたクレームもここに格納されます。今回指定したペイロードの型には Role と Permission というプロパティが定義されています。その値が格納されていることが分かります。

[0] {userpayload: {"user":{"Role":2,"Permissions":1}}}
[1] {jti: ff28ffc3-6709-4de2-a914-e5aa88594fd8}
[2] {nbf: 1553426924}
[3] {exp: 1553427228}
[4] {iat: 1553426929}
[5] {iss: testIssuer}
[6] {aud: testAudience}

今回の検証では out 引数 token からもクレーム情報を取得することはできました。
SecurityToken は基底型であり、実際に返された型は System.IdentityModel.Tokens.Jwt.JwtSecurityToken でした。この型には Claims プロパティが定義されており、上記と同じ内容の情報を取得することができました。但し、常にこの型が返されるのか、その他の型が返されることがあるのかは分かりません。

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

C#のDI ContainerのIDisposableへの参照保持について

C#のDI Containerはいろいろ (https://github.com/danielpalme/IocPerformance) 存在します。一部のコンテナでは自動DisposeのためにIDisposableインスタンスへの参照が保持(Tracking)され、注意しないとMemoryOverflowExceptionが発生することがあります。Trackingするコンテナとそうでないコンテナを調査してみました。

結果

各種DI Containerをできるだけデフォルト状態で調査したところ、以下のようになりました。

DI Container Normal Disposable
Autofac No Track
LightInject No No
abioc No No
DryIoc(NoTrack) No No
DryIoc(Track) No Track
Grace No Track
MicroResolver No No
Unity.Container No No

AutofacとGraceがIDisposableをTrackingするようです。

DryIocは2つありますが、DryIocだとどちらの挙動がよいか設定で選択できます。なにも選択しない状態だとIDisposableを実装する型を登録するとエラーが発生し、Trackingするかどうかを指示するように警告されるので、結構親切ですね。

なお、調査したときのバージョンは以下のとおりです。

  • Autofac, Version=4.9.1.0, Culture=neutral, PublicKeyToken=17863af14b0044da
  • LightInject, Version=5.4.0.0, Culture=neutral, PublicKeyToken=null
  • abioc, Version=0.7.0.0, Culture=neutral, PublicKeyToken=null
  • DryIoc, Version=4.0.0.0
  • Grace, Version=6.4.2.0, Culture=neutral, PublicKeyToken=b7d24c6367970497
  • MicroResolver, Version=2.3.5.0, Culture=neutral, PublicKeyToken=null
  • Unity.Container, Version=5.10.2.0, Culture=neutral, PublicKeyToken=489b6accfaf20ef0

調査の発端

ここからは余談ですが、

C#アプリでAutofacを使用していたのですが、バッチ処理とかで大量のクラス生成を繰り返す処理を実行すると、アプリ内ではどこからも参照されていないのに、インスタンスが解放されず、OutOfMemoryExceptionが発生する問題に遭遇しました。

検索すると以下の記事がヒットします。

https://nblumhardt.com/2011/01/an-autofac-lifetime-primer/

Autofac holds references to all the disposable components it creates

「Autofacは生成したすべてのIDsposableインスタンスへの参照を保持する」ので、

以下のようにLifetimeScopeで囲ってあげないとMemoryLeakしてMemoryOverflowExceptionになる。lifetimeScope.Resolve<IMyResource>();container.Resolve<IMyResource>();と書いてしまうとMemoryLeakになるということですね。

// var container = …
while (true)
{
    using (var lifetimeScope = container.BeginLifetimeScope())
    {
        var r = lifetimeScope.Resolve<IMyResource>();
        // r, all of its dependencies and any other components
        // created indirectly will be released here
    }
}

Don’t resolve from the root container. Always resolve from and then release a lifetime scope.

「常にlifetime scopeからインスタンスを生成し、タスク完了時にscopeをdisposeしなければならない」

Autofacの思想としては、大量データの生成、破棄をともなう繰り返し処理はコンテナに依存するようにし、厳密にLifetime管理せよ、ということなのでしょう。Autofacは参照しているIDisposableインスタンスのDispose()を自動で呼び出してくれるようで、それも便利なケースもありそうですね。

Autofacより後発のLightInjectではこのようなメモリリークは発生しません。前掲の記事によると、TrackingなDI ContainerとそうでないDI Containerがあるようです。そこで、C#のDI ContainerをTrackingなDI Containerとそれ以外で分類してみました。

調査方法

InnerRunメソッドのスコープ内でResolveしたオブジェクトのFinalizeをcallbackで通知を受け、GC.Collect()後にFinalizeされたかどうかを返します。

public bool Run(Type t)
{
    var container = CreateContainer(t);
    var finalized = false;
    InnerRun(container, t, () => {finalized = true;});

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    return finalized;
}

private void InnerRun(object container, Type t, Action callback)
{
    // Resolve in this scope.
    var obj = Resolve(container, t);
    obj.FinalizeCallback = callback;

    // Scope out, then no reference to obj.
}

Finalizeを監視する対象のBaseクラスは以下のようにしました。これを通常のオブジェクト(Normal)とIDisposableを実装したオブジェクトそれぞれに継承させるようにしています。

public abstract class FinalizeCallbackable
{
    public Action FinalizeCallback {get; set;} = null;

    ~FinalizeCallbackable()
    {
        Debug.WriteLine($"Finalize({this.GetType()})");
        FinalizeCallback?.Invoke();
    }
}

ソースコードはこちらです。.NET Core 2.2で動作確認しました。
https://github.com/youmts/dicontainer-tracking-checker/blob/master/Model/IFinalizeCallbackable.cs

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

Unity uGUIのButtonのイベント(PointerUp, PointerDown)を全てスクリプトから設定する

古い記事です。
==> https://qiita.com/romaroma/items/b632bd8d69271a04c61e
の記事を参考にしてください。

個人的な覚書です。
余計な処理もついています。適宜コメントアウトしてください。

PointerDownなら、onClick.AddListener()一行で行けるのですが、PointerUpは別途EventTriggerをアタッチして、設定しています。これによって、全てのイベントをスクリプトから設定できるようになります。
関数の引数が複雑になっているのは、引数に別の関数を与える、いわゆるC言語の関数ポインタのようなことをやっているからです。
AddEvent関数の第一引数は、Buttonクラスですが、基底クラスにすれば、別のuGUIコンポーネントでも対応できるはずです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

namespace Roma
{
    public class Roma_UI : MonoBehaviour
    {
        // Start is called before the first frame update
        public Dictionary<string, AudioClip> strAudioMap;
        void Start()
        {
            Canvas c = GenCanvasAndEventSystem();
            Button b = GenButton(c);

            AddEvent(b, EventTriggerType.PointerDown, e => { PointerDown(b.gameObject); });
            AddEvent(b, EventTriggerType.PointerUp, e => { PointerUp(b.gameObject); });

            AudioClip[] musicalSscales = Resources.LoadAll<AudioClip>("NoteSoundData/C4toC6");
            strAudioMap = new Dictionary<string, AudioClip>();
            for (int i = 0; i < musicalSscales.Length; i++) 
            {
                AudioClip ac = musicalSscales[i];
                string[] token = ac.name.Split('_');
                strAudioMap.Add(token[1], ac);
                print(i + " " + token[1] + " " + token[2]);
            }
        }

        // Update is called once per frame
        void Update()
        {

        }

        static void AddEvent(Button button, EventTriggerType type, UnityEngine.Events.UnityAction<BaseEventData> call)
        {
            EventTrigger.Entry entry = new EventTrigger.Entry();
            entry.eventID = type;
            entry.callback.AddListener(call);
            button.gameObject.AddComponent<EventTrigger>().triggers.Add(entry);
        }

        static Canvas GenCanvasAndEventSystem()
        {
            Canvas canvas = new GameObject().AddComponent<Canvas>();
            canvas.name = "Roma_Canvas";
            canvas.renderMode = RenderMode.ScreenSpaceOverlay;
            canvas.gameObject.AddComponent<GraphicRaycaster>();

            EventSystem eventSystem = new GameObject().AddComponent<EventSystem>();
            eventSystem.gameObject.AddComponent<StandaloneInputModule>();
            eventSystem.name = "Roma_EventSystem";

            return canvas;
        }

        static Button GenButton(Canvas canvas)
        {
            Button button = new GameObject().AddComponent<Button>();
            button.name = "Roma_Button";
            button.targetGraphic = button.gameObject.AddComponent<Image>();
            button.GetComponent<RectTransform>().SetParent(canvas.transform);

            return button;
        }

        void PointerUp(GameObject g)
        {
            print("up");
        }

        void PointerDown(GameObject g)
        {
            print("down");
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画面上のQRコードをデコードするCsharpのフォームアプリを作る

下記、記事を発展させて、画面上に表示されたQRコードを、読みこむフォームを作る


QRCodeEncoderDecoderLibraryを用いてQRコードをデコードする(読み込む)Csharpの単体アプリを作る
https://qiita.com/santarou6/items/7b3a449f7f794639f252


qr_decode_frm
//c:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /reference:QRCodeEncoderDecoderLibrary.dll /target:winexe qr_decode_frm.cs

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Imaging;
using QRCodeEncoderDecoderLibrary;

namespace qr_decode_frm
{
public class Form1 : Form
{

    public Button button1;

    public Form1()
    {
    this.TransparencyKey = this.BackColor;
    button1 = new Button();
    button1.Size = new Size(40, 40);
    button1.Location = new Point(0, 0);
    button1.Text = "★";
    this.Controls.Add(button1);
    button1.Click += new EventHandler(button1_Click);
    }

    private void button1_Click(object sender, EventArgs e)
    {

    try{

    Rectangle rc = this.ClientRectangle;
    Point p = this.PointToScreen(new Point(0, 0));
    double rt = 1.5;  //96dpi_rt=1, 144dpi_rt=1.5
    p.X = (int)(p.X * rt);
    p.Y = (int)(p.Y * rt);
    rc.Width = (int)(rc.Width * rt);
    rc.Height = (int)(rc.Height * rt);
    Bitmap bmp = new Bitmap(rc.Width, rc.Height, PixelFormat.Format32bppArgb);
    var g = Graphics.FromImage(bmp);
    g.CopyFromScreen(p.X, p.Y, 0, 0,rc.Size, CopyPixelOperation.SourceCopy);

    QRDecoder   QRCodeDecoder;
    Bitmap      QRCodeInputImage;

    QRCodeDecoder = new QRDecoder();
    QRCodeInputImage = bmp;
    byte[][] DataByteArray = QRCodeDecoder.ImageDecoder(QRCodeInputImage);

    //string Result = System.Text.Encoding.GetEncoding(932).GetString(DataByteArray[0]);
    string Result = System.Text.Encoding.UTF8.GetString(DataByteArray[0]);
    //string Result = System.Text.Encoding.GetEncoding(51932).GetString(DataByteArray[0]);

    MessageBox.Show(Result);
    //Clipboard.SetDataObject(Result,true,20,500);

    }
    catch(Exception ex){
        MessageBox.Show("読み取りエラー\n\n"+ex);
    }

    }

    [STAThread]
    static void Main()
    {
    Application.EnableVisualStyles();
    Application.Run(new Form1());
    }

}
}

結果
kekka.PNG

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

数値リテラルの拡張メソッド呼び出しでハマった

数値に対する拡張メソッドを書いていて、リテラルに対して実行したときにハマったのでメモ。

例えば、以下のような拡張メソッドがあったとして、

public static class NumEx
{
  public static double Ceiling(this double num) => Math.Ceiling(num);
}

次のように負数の数値リテラルに対して実行すると期待した値とは異なる値が返ってくる。

Console.log(-1.5.Ceiling()); // --> -1.0を期待
> -2 // しかし実行すると-2.0が返る

これは演算子の優先順位の問題で、マイナスの単項演算子の優先順位が拡張メソッドの
呼び出しよりも弱いために、拡張メソッドの呼び出し結果に対してマイナスの単項演算子
が評価されているためこうなってしまう。

Console.log(-1.5.Ceiling());   // <- この記述は
Console.log(-(1.5.Ceiling())); // <- このように解釈される

回避するには、リテラルに対して直接拡張メソッドを実行するのではなく、いったん変数に
格納してやるか、()で括って優先順位を明示的に示してやればよい。

var n = -1.5;
Console.log(n.Ceiling());
> -1
Console.log((-1.5).Ceiling());
> -1

リテラルに対して拡張メソッドを実行するときは意識しておかないと、忘れたころにまたハマりそう。

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

C# SHObjectProperties関数を使用してファイル、プリンター、ボリュームのプロパティダイアログを開く

概要

C#でファイル、プリンター、ボリュームのプロパティダイアログを開く方法にはSHFileOperationEx関数、IShellFolderインターフェイスとIContextMenuインターフェイスがありますが、SHObjectProperties関数を使用することでより簡単に実現することができます。以下ではそのサンプルコードを紹介します。

サンプルコード

ファイル(C:\Windows)とプリンター(最初に見つかったプリンター)のプロパティを表示するサンプルコードです。GUIDパスを指定したボリュームのプロパティ表示にはGUIDパスの取得が別途必要となります。

using System;
using System.Drawing.Printing; // プリンターの列挙に必要
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            // プリンタのプロパティ
            var printers = PrinterSettings.InstalledPrinters;
            if (printers.Count != 0)
            {
                Utility.ShowObjectProperties(
                    ObjectPropertiesObjectType.PrinterName,
                    printers[0]);
            }

            // ファイルのプロパティ
            Utility.ShowObjectProperties(
                ObjectPropertiesObjectType.FilePath,
                @"C:\Windows");
        }
    }

    public static class Utility
    {
        private static class NativeMethods
        {
            [DllImport("shell32.dll")]
            public static extern bool SHObjectProperties(
                IntPtr hwnd,
                uint shopObjectType,
                [MarshalAs(UnmanagedType.LPWStr)] string pszObjectName,
                [MarshalAs(UnmanagedType.LPWStr)] string pszPropertyPage);
        }

        public static bool ShowObjectProperties(
            ObjectPropertiesObjectType objectType,
            string objectName,
            string pageName = null,
            IntPtr windowHandle = default(IntPtr))
        {
            return NativeMethods.SHObjectProperties(
                windowHandle,
                (uint)objectType,
                objectName,
                pageName);
        }
    }

    public enum ObjectPropertiesObjectType : uint
    {
        // プリンタのフレンドリーネーム SHOP_PRINTERNAME
        PrinterName = 0x00000001,
        // 絶対パス SHOP_FILEPATH
        FilePath = 0x00000002,
        // ボリュームGUID SHOP_VOLUMEGUID
        VolumeGUID = 0x00000004
    }
}

参考

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

SHObjectProperties関数を使用してファイル、プリンター、ボリュームのプロパティダイアログを開く

概要

C#でファイル、プリンター、ボリュームのプロパティダイアログを開く方法にはSHFileOperationEx関数、IShellFolderインターフェイスとIContextMenuインターフェイス等がありますが、SHObjectProperties関数を使用することでより簡単に実現することができます。ただし、SHObjectProperties関数はWindows XP/Windows Server 2003以上(デスクトップアプリケーション)でしか実装されていません。

以下ではSHObjectProperties関数を使用したサンプルコードを紹介します。

サンプルコード

ファイル(C:\Windows)とプリンター(最初に見つかったプリンター)のプロパティを表示するサンプルコードです。GUIDパスを指定したボリュームのプロパティ表示にはGUIDパスの取得が別途必要となります。

using System;
using System.Drawing.Printing; // プリンターの列挙に必要
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            // プリンタのプロパティ
            var printers = PrinterSettings.InstalledPrinters;
            if (printers.Count != 0)
            {
                Utility.ShowObjectProperties(
                    ObjectPropertiesObjectType.PrinterName,
                    printers[0]);
            }

            // ファイルのプロパティ
            Utility.ShowObjectProperties(
                ObjectPropertiesObjectType.FilePath,
                @"C:\Windows");
        }
    }

    public static class Utility
    {
        private static class NativeMethods
        {
            [DllImport("shell32.dll")]
            public static extern bool SHObjectProperties(
                IntPtr hwnd,
                uint shopObjectType,
                [MarshalAs(UnmanagedType.LPWStr)] string pszObjectName,
                [MarshalAs(UnmanagedType.LPWStr)] string pszPropertyPage);
        }

        public static bool ShowObjectProperties(
            ObjectPropertiesObjectType objectType,
            string objectName,
            string pageName = null,
            IntPtr windowHandle = default(IntPtr))
        {
            return NativeMethods.SHObjectProperties(
                windowHandle,
                (uint)objectType,
                objectName,
                pageName);
        }
    }

    public enum ObjectPropertiesObjectType : uint
    {
        // プリンタのフレンドリーネーム SHOP_PRINTERNAME
        PrinterName = 0x00000001,
        // 絶対パス SHOP_FILEPATH
        FilePath = 0x00000002,
        // ボリュームGUID SHOP_VOLUMEGUID
        VolumeGUID = 0x00000004
    }
}

参考

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

C# SearchPath関数を使用して既定の順番でパスを検索する

概要

Windowsではファイル名を指定された場合のパスの検索順序がレジストリに保存されており、「ファイル名を指定して実行」やLoadLibrary関数等ではこの順序が使用されます。この検索機能はSearchPath関数で公開されており、C#でもP/Invokeにより使用することができます。以下ではSearchPath関数の使用例を紹介します。

SearchPath関数の使用例

コード

using System;
using System.Runtime.InteropServices;
using System.Text;

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            var user32DllPath = Utility.SearchPath("user32.dll");
        }
    }

    public static class Utility
    {
        private static class NativeMethods
        {
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern uint SearchPath(
                [In]string lpPath,
                [In]string lpFileName,
                [In]string lpExtension,
                uint nBufferLength,
                StringBuilder lpBuffer,
                IntPtr lpFilePart);
        }

        /// <summary>
        /// ファイルの場所を検索します。
        /// </summary>
        /// <param name="fileName">検索する名前を指定します。</param>
        /// <param name="path">検索する場所を指定します。nullの場合はシステムの既定の場所を検索します。nullと空白は区別されます。</param>
        /// <param name="extension">検索する拡張子を指定します。nullと空白は区別されます。</param>
        /// <returns></returns>
        public static string SearchPath(
            string fileName,
            string path = null,
            string extension = null)
        {
            var len = NativeMethods.SearchPath(
                path, fileName, extension,
                0, null, IntPtr.Zero);
            var buffer = new StringBuilder((int)len);
            var ret = NativeMethods.SearchPath(
                path, fileName, extension,
                (uint)buffer.Capacity, buffer, IntPtr.Zero);
            if (ret == 0 && Marshal.GetLastWin32Error() != 0)
            {
                return null;
            }
            else
            {
                return buffer.ToString(0, (int)ret);
            }
        }
    }
}

出力例

user32DllPath: "C:\\WINDOWS\\SYSTEM32\\user32.dll"

参考

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

【Unity(C#)】キーボードの同時入力を禁止する

UnityのInput(キーボード入力)は同時入力できてしまう

Input.GetKeyDownを例に話を進めていきます。
こちらの記事にもありますように、
指定したキーを押した瞬間、1フレームだけ呼び出されてtrueを返します。

もし同時に押したらどうなるのか検証してみました。

void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            print("A");
        }
        if (Input.GetKeyDown(KeyCode.S))
        {
            print("S");
        }
    }

Key同時押し.png

このように両方が同じように増えました。
キー入力は人間の手で行っているので
厳密に同時(同一フレーム上)に増えているかどうかは読み取れませんが、
前回の記事で発生したバグは
同一フレーム上でキー入力を受け取ってしまったことが原因かと思われます。

Bag.png

矢印キーの入力が同時に行われてしまったことでこのようなバグが発生しています。
※正しい挙動は前回の記事を参照ください

同時(同一フレーム)入力できるのはヤバい

ヤバいです。
例えば、自動販売機。
ふざけてボタンを同時押ししたことがあるかと思います。(ない?)

あれがもし奇跡的なタイミングで両方のボタンをぴったり同時に押した場合、
反応してしまう仕組みだったとしたらどうでしょうか。
一本タダで手に入ってしまいます。それくらいヤバいです。

とてもわかり易い例えは置いといて、
同時押しを想定していないのであれば、バグの温床になりかねません。

なので、キーボードは片方づつ押せるようになっていてほしいものです。

namespaceを使ってみた

namespaceはコンポーネントをいっぱい登録して簡単にどこでも呼び出しやすくしました
みたいな理解をしています。(合ってますか?)

Unityのスクリプトリファレンスには
大規模な開発にもってこいです みたいなことが書いてありました。
(それ以外に使う理由ってありますか?よく使う方見てたらコメントください)

現時点であまり縁がないですが使ってみようと思います。

using UnityEngine;

namespace InputKey{
    /// <summary>
    /// 同時入力を禁止する
    /// </summary>
    public static class MyInput 
    {
        static bool isCheck_Input;

       public static bool MyInputKeyDown(KeyCode key)
        {
            if(Input.anyKeyDown == false) isCheck_Input = false;

            if (isCheck_Input==false)
            {
                if (Input.GetKeyDown(key))
                {
                    isCheck_Input = true;
                    return true;
                }
            }
            return false;
        }
    }
}

このスクリプトは何にもAdd Componentしなくていいやつっぽい(staticなので)です。

次は使う側です。using InputKeyを名前空間に追加します。

using UnityEngine;
using InputKey;

public class Test : MonoBehaviour
{
    void Update()
    {
        if (MyInput.MyInputKeyDown(KeyCode.A))
        {
            print("A");
        }

        if (MyInput.MyInputKeyDown(KeyCode.S))
        {
            print("S");
        }
    }
}

これで同時入力(同一フレーム入力)できなくなってました:ok_hand:

void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            print("A");
        }
        else if (Input.GetKeyDown(KeyCode.S))
        {
            print("S");
        }
    }

else if使えば同一フレームを回避できるらしいです!
ただ、1フレームずれただけだと見た目ではあまり変化ないです。

もっと厳格な同時押し禁止

MyInputKeyDownにしろ、else ifにしろ、
同時入力が避けられるのは1フレームのみです。

キーを押し込んでいる間は他のキーを押せない状態こそ
真の同時押し禁止といえるでしょう。

 //真の同時押し不可
        public static bool MyInputKey(KeyCode key)
        {
            if(Input.anyKey == false) isCheck_Input = false;

            if (isCheck_Input==false)
            {
                if (Input.GetKey(key))
                {
                    isCheck_Input = true;
                    return true;
                }
            }
            return false;
        }

これで
押しっぱなしでも一回しか反応しない、かつキーを押し込んでいる間は他のキーを押せない状態になりました。

これが真の同時押し禁止だーーー!

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

技術書 読んだ順番を振り返ってみた

必要に迫られてSQLを勉強し始めて以来、購入した技術書も気づけば10冊を超えていました。本棚を晒すようで若干恥ずかしさもあるのですが、自身の振り返りも兼ねて記事にしてみました。
プログラミング初心者が、ちょっとしたデータベース構築と入力画面の作成ができるようになるまでに読んだ本の一覧として、参考になれば幸いです。

読んだ順番

  1. SQL Server 2016の教科書
  2. SQLパズル 第2版
  3. SQLアンチパターン
  4. プログラマのためのSQL 第4版
  5. 独習C# 新版
  6. もっとプログラマ脳を鍛える数学パズル
  7. やってはいけないデザイン
  8. リーダブルコード
  9. 実戦で役立つC#プログラミングのイディオム/定石&パターン
  10. Effective C# 6.0/7.0
  11. プリンシプル オブ プログラミング

この順番で読んだほうが良かったかも順

  1. プリンシプル オブ プログラミング
  2. リーダブルコード
  3. プログラマのためのSQL 第4版
  4. SQLアンチパターン
  5. SQLパズル 第2版
  6. SQL Server 2016の教科書
  7. 独習C# 新版
  8. 実戦で役立つC#プログラミングのイディオム/定石&パターン
  9. Effective C# 6.0/7.0
  10. もっとプログラマ脳を鍛える数学パズル
  11. やってはいけないデザイン

発売順を考慮していないため、実際にできたかは別として、その分野の原理原則的なことを記している書籍を先に読んでおいたほうが、後続の本の理解もしやすかっただろうと思いました(とはいえ、その時の気分や勢いがあるからこそ一気に読めるというのもあるので難しいですね)。

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

UnityのTest Runnerでusingが効かなかった

Test Runnerでusingが効かなかった

Unityでユニットテストを書くなら、公式の Test Rnner がよいと聞いたので、試したところ、テストコード内の using でエラーが発生しました。

LeapMotionのSDKを利用したコードだったので、 using Leap を指定したのですが、ご覧の通り赤い波線が表示されました。

TestCode2.png

カーソルを合わせると、ポップアップで警告文が表示されました。

TestCode.png

型または名前空間の名前 'Leap' が見つかりませんでした(using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)。

と書かれています。

テストコードではない通常のコードからは Leap が参照できているのに、なぜかテストコードからは参照できていないようです。

Leap の Assembly Definition が必要だった

原因を調査したところ、こちらの記事を見つけました。
Unity 2018.1でTest Runnerの使い方が変わっていた話

Assembly Definition が必要ということが書かれています。私の環境も、まさに、これが原因でした。

LeapMotion.asmdef を作る

LeapMotionのフォルダで右クリック → CreateAssenbly Definition を選択すると、 NewAssembly という名前で入力待機状態になったので、 LeapMotion と入力したところ、 LeapMotion.asmdef というファイルが生成されました。

LeapMotion.asmdef をテストコード側から参照する

これをテストコード側のasmdefファイルから参照するようにします。

私の環境の場合、PlayModeテスト用に PlayMode.asmdef というファイルを作っていたので、Unity内でそれを選択し、 InspectorReferences の右下の +None (Assembly Definition Asset) の右の → リストの中から LeapMotion を選択することで、参照設定ができました。

image.png

これでテストコードの using Leap のエラーが解消されました。

補足:Enable playmode tests for all assemblies

PlayModeでテストしようとすると色々なエラーが出ました。
私の環境では、 Enable playmode tests for all assemblies の設定をしたら、解消されました。

この設定に関しては、こちらの記事が丁寧でわかりやすかったので、詳しくはご参照ください。
Unity TestRunner(PlayMode)のアセンブリ参照問題

さいごに

ユニットテストをしたいだけなのに、 Assembly Definition という設定が必要なのは、JavaのJUnitやRubyのminitestとかと比べると面倒大変だと感じました。
ただ、Assemblyを定義することで、ビルドの高速化等も可能になるようです。せっかくなので勉強していこうと思います。

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

dotnet ef migrations でエラーになった話

結論

  • ASP.NET Core 2.2 アプリで dotnet ef コマンドを実行する場合は、 Program.csCreateWebHostBuilder メソッドが必要。
    • ここから EF Core CLI が ASP.NET Core アプリの情報 (DB 接続先とか) にアクセスする。
    • ただし、.NET Core 3.0 だと名前が変わるから注意! (CreateHostBuilder)
  • dotnet ef コマンドのエラー調査には -v オプション (--verbose) を使おう

事象

ASP.NET Core & EFCore のチュートリアルを触っていて、DB マイグレーションができなかった。
C# コードでモデルを定義し、 EF Core のマイグレーションコマンド (dotnet ef migrations add InitialCreate) を実行したところ、エラー発生。

$ dotnet ef migrations add InitialCreate
Unable to create an object of type 'BloggingContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728

-v オプションで詳細表示したところ、原因が判明した。

$ dotnet ef migrations add InitialCreate -v
Using project 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\AspNetSample.csproj'.
Using startup project 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\AspNetSample.csproj'.
Writing 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\obj\AspNetSample.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=C:\Users\wukan\AppData\Local\Temp\tmpF769.tmp /verbosity:quiet /nologo C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\AspNetSample.csproj
Writing 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\obj\AspNetSample.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=C:\Users\wukan\AppData\Local\Temp\tmp24.tmp /verbosity:quiet /nologo C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\AspNetSample.csproj
dotnet build C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\AspNetSample.csproj /verbosity:quiet /nologo

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:06.14
dotnet exec --depsfile C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\bin\Debug\netcoreapp3.0\AspNetSample.deps.json --additionalprobingpath C:\Users\wukan\.nuget\packages --additionalprobingpath "C:\Program Files\dotnet\sdk\NuGetFallbackFolder" --runtimeconfig C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\bin\Debug\netcoreapp3.0\AspNetSample.runtimeconfig.json "C:\Program Files\dotnet\sdk\3.0.100-preview3-010431\DotnetTools\dotnet-ef\3.0.0-preview3.19153.1\tools\netcoreapp3.0\any\tools\netcoreapp2.0\any\ef.dll" migrations add InitialCreate --assembly C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\bin\Debug\netcoreapp3.0\AspNetSample.dll --startup-assembly C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\bin\Debug\netcoreapp3.0\AspNetSample.dll --project-dir C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\ --language C# --working-dir C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample --verbose --root-namespace AspNetSample
Using assembly 'AspNetSample'.
Using startup assembly 'AspNetSample'.
Using application base 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\bin\Debug\netcoreapp3.0'.
Using working directory 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample'.
Using root namespace 'AspNetSample'.
Using project directory 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\'.
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Finding application service provider...
Finding IWebHost accessor...
No CreateWebHostBuilder(string[]) method was found on type 'AspNetSample.Program'.
No application service provider was found.
Finding DbContext classes in the project...
Found DbContext 'BloggingContext'.
Using context 'BloggingContext'.
System.InvalidOperationException: No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext.
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.Initialize(IServiceProvider scopedProvider, IDbContextOptions contextOptions, DbContext context)
   at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
   at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>.get_Instance()
   at Microsoft.EntityFrameworkCore.Internal.InternalAccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(Func`1 factory)
   at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(String contextType)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_1.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext.

No CreateWebHostBuilder(string[]) method was found on type 'AspNetSample.Program'. というメッセージに注目。どうやら、EF Core CLI は ASP.NET Core アプリケーションの Program クラスに定義されている (であろう) CreateWebHostBuilder メソッドを必要とする模様。これはプロジェクト作成時 (dotnet new) に自動生成されているハズ…なのだが、自分の環境では違う名前になっていたため、エラーが発生した。

解決方法

Program.csCreateWebHostBuilder メソッドをちゃんと定義し、

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateWebHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

再度マイグレーションコマンド実行。

dotnet ef migrations add InitialCreate -v
Using project 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\AspNetSample.csproj'.
Using startup project 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\AspNetSample.csproj'.
Writing 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\obj\AspNetSample.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=C:\Users\wukan\AppData\Local\Temp\tmpA315.tmp /verbosity:quiet /nologo C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\AspNetSample.csproj
Writing 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\obj\AspNetSample.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=C:\Users\wukan\AppData\Local\Temp\tmpA9DD.tmp /verbosity:quiet /nologo C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\AspNetSample.csproj
dotnet build C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\AspNetSample.csproj /verbosity:quiet /nologo

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:03.59
dotnet exec --depsfile C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\bin\Debug\netcoreapp3.0\AspNetSample.deps.json --additionalprobingpath C:\Users\wukan\.nuget\packages --additionalprobingpath "C:\Program Files\dotnet\sdk\NuGetFallbackFolder" --runtimeconfig C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\bin\Debug\netcoreapp3.0\AspNetSample.runtimeconfig.json "C:\Program Files\dotnet\sdk\3.0.100-preview3-010431\DotnetTools\dotnet-ef\3.0.0-preview3.19153.1\tools\netcoreapp3.0\any\tools\netcoreapp2.0\any\ef.dll" migrations add InitialCreate --assembly C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\bin\Debug\netcoreapp3.0\AspNetSample.dll --startup-assembly C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\bin\Debug\netcoreapp3.0\AspNetSample.dll --project-dir C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\ --language C# --working-dir C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample --verbose --root-namespace AspNetSample
Using assembly 'AspNetSample'.
Using startup assembly 'AspNetSample'.
Using application base 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\bin\Debug\netcoreapp3.0'.
Using working directory 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample'.
Using root namespace 'AspNetSample'.
Using project directory 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\'.
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Finding application service provider...
Finding IWebHost accessor...
Using environment 'Development'.
Using application service provider from IWebHost accessor on 'Program'.
Found DbContext 'BloggingContext'.
Finding DbContext classes in the project...
Using context 'BloggingContext'.
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 2.2.3-servicing-35854 initialized 'BloggingContext' using provider 'Pomelo.EntityFrameworkCore.MySql' with options: ServerVersion 8.0.15 MySql
Finding design-time services for provider 'Pomelo.EntityFrameworkCore.MySql'...
Using design-time services from provider 'Pomelo.EntityFrameworkCore.MySql'.
Finding design-time services referenced by assembly 'AspNetSample'.
No referenced design-time services were found.
Finding IDesignTimeServices implementations in assembly 'AspNetSample'...
No design-time services were found.
Writing migration to 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\Migrations\20190323131849_InitialCreate.cs'.
Writing model snapshot to 'C:\Users\wukan\Documents\work\dotnetcore\SqlPuzzle\src\AspNetSample\Migrations\BloggingContextModelSnapshot.cs'.
Done. To undo this action, use 'ef migrations remove'

成功!٩(๑´0`๑)۶
Using application service provider from IWebHost accessor on 'Program'. とあるように、今度はちゃんと ASP.NET Core アプリケーションの情報を読み込んでくれた模様。

蛇足

この CreateWebHostBuilder というメソッド、 .NET Core 2.2 -> 3.0 で名前が変更になる (CreateHostBuilder)。よって .NET Core バージョンアップ時には注意が必要。

実は過去 (.NET Core 2.0 -> 2.1) にも名前が変わっている模様。
https://github.com/aspnet/Docs/issues/7087
名前が安定しない… (´・ω・`)

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