20191219のC#に関する記事は19件です。

[C#] async な lock をしよう

C# では,非同期なメソッドでは lock が使えません.この記事ではそれでも lock したいときはどうするのっていうお話をします.

たとえば,こんなふうにダブルチェックロッキングしたいとしますね.

// これが複数のスレッドから非同期に呼ばれる
private static async ValueTask 条件満たしたらなんかするAsync()
{
    if (条件)
    {
        lock (_lockObj) 
        {
            if (条件)
            {
                await なんかすごく重いIOバウンドなやつAsync();
            }
        }
    }
}

lock ステートメントは非同期メソッド内で使えないので,実際にはこのコードはコンパイルが通りません.
じゃあどうしようか,といって,一番ラクな逃げ道は同期にしちゃうことです.カンタンカンタン.

// これが複数のスレッドから非同期に呼ばれる
private static void 条件満たしたらなんかする()
{
    if (条件)
    {
        lock (_lockObj) 
        {
            if (条件)
            {
                なんかすごく重いIOバウンドなやつAsync().Wait();
            }
        }
    }
}

Task だって Wait しちゃえばただの同期,これはちゃんと動きます.でもここで「いやなんのための async なんだよ」ってなりますよね. lock したいだけなのに,そのために非同期の恩恵を捨て去る,そんなことやっちゃダメです.

じゃあ最新技術をこね回して難しいコード書くのかっていうとそんなことはなく,むしろ全く逆で古くからある技術を使います.そう,セマフォ です.
セマフォっていうと OS の機能で,プロセス間の資源のアクセス制御に使うイメージですが,.NET にはプロセス内で利用するための SemaphoreSlim クラスがあります.
セマフォといっても結局待たなきゃいけないでしょって話なんですが, SemaphoreSlim クラスには非同期で待てる WaitAsync() メソッドがあるわけです.
これをつかうと完全に非同期な lock が実現できるわけですね.
これはもうパターンが固定なので,ちょっと汎用的に AsyncLock なんていうクラスを作っておくとどこでも使えます.
やってることもとてもカンタンなので,作り方さえ覚えてしまえばとっさのときにも書けます.

/// <summary>
/// async な文脈での lock を提供します.
/// Lock 開放のために,必ず処理の完了後に LockAsync が生成した IDisposable を Dispose してください.
/// </summary>
public sealed class AsyncLock
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public async Task<IDisposable> LockAsync()
    {
        await _semaphore.WaitAsync();
        return new Handler(_semaphore);
    }

    private sealed class Handler : IDisposable
    {
        private readonly SemaphoreSlim _semaphore;
        private bool _disposed = false;

        public Handler(SemaphoreSlim semaphore)
        {
            _semaphore = semaphore;
        }

        public void Dispose()
        {
            if (!_disposed)
            {
                _semaphore.Release();
                _disposed = true;
            }
        }
    }
}

面倒な人はこれをコピペでいいです.使うときは, lock 構文の代わりに using 構文を使います.これによって, semaphore の管理を忘れて lock という意味を持たせた見た目のコードを書けます.

たとえば,最初の例を書いてみるとこんな感じです.

private static readonly s_lock = new AsyncLock();
private static async Task 条件満たしたらなんかするAsync()
{
    if (条件)
    {
        using (await s_lock.LockAsync()) 
        {
            if (条件)
            {
                await なんかすごく重いIOバウンドなやつAsync();
            }
        }
    }
}

lock が using に変わっただけで,あとはあまり変わりません.でもこれはコンパイルも通るし,ちゃんと非同期でパフォーマンスよく動きます.

というわけで,いままで .Wait しちゃってた方,今日からは await しましょう!

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

UnityでMonaparty(その2)

2019年のまとめ(その2)です。

はじめに

UnityでMonaparty(その1)ではMonapartyのAPIを使って様々なデータを取得、加工して遊んでみました。

前述したとおりAPIを使ってトークンの送金、発行も可能です。今回は「送金」について書いてみます。ちなみにこの送金が出来るまでに自分は3か月くらいかかったのですが記念にその期間のことも最後に書いてみます。

また「送金」周りはシビアですので各自で検証をお願いします。

NBitcoinを使う(1)

「モナコインなのに?」

送金をするには署名をするわけですが、その「署名」をするのに使用します。

NBitcoinとはBitcoinをC#で扱うためにNicolas Dorierさんが開発されたライブラリです。

また、そのNBitcoinでアルトコインを使用するため、各コインのdeveloperさん達によりコードが追加されております。そこにはなんとモナコインもあり、developerさんにとても感謝です。(下記リンクよりどなたか確認してみましょう)

NBitcoin
NBitcoin.Altcoins

この2つをVisual Studio等から何というかその、、、"NuGet"します。

NBitcoinを使う(2)

その次にNBitcoinをコンパイル?します。
詳しくはこちら

git clone https://github.com/MetacoSA/NBitcoin/
cd NBitcoin/NBitcoin
dotnet publish -c Release -f netstandard2.0
Remove-Item -Force -Recurse .\bin\Release\netstandard2.0\publish\runtimes\

すると「netstandard2.0」というファイルが出来たかと思います。

そしてここがポイントなのですが、その中のファイルから
・「NBitcoin」
・「NBitcoin.Altcoins」
・「System.Buffers」
・「Newtonsoft.Json」
・「Microsoft.Extensions.Logging.Abstractions」

だけを残し、Unityの「Assets」の中へ放り込みます。
エラーが出る場合は色々試してみてください。(たぶんJson周りで出るかも?)

使い方

下記よりNBitcoinの使い方が学べますので、とりあえずBitcoinのTestnet環境で試してみましょう。
Programming The Blockchain in C# 日本語

ただここで1つ落とし穴があり、自分はここで沼にハマり長い間浸かっていました。と言うのもNBitcoinではQBitNinjaという"Transactionの中身を良い感じに整理してくれるもの"を使っており、モナコインでは工夫が必要です。

結果的に署名前までのTransactionを組み上げてくれる「create_send」というAPIを使うのですが、そこに至るまでの余談は話がずれるので最後に書きます。

コード

未署名Txのリクエスト.cs
using NBitcoin;
using NBitcoin.Altcoins; //予測変換にMonacoinが出るようになります。

    private string unSignTxHex;  //取得した未署名Txを入れる
    private string signedTxHex;  //署名済みTxを入れる

    //Jsonの組み立て
    [Serializable]
    class MonapaApi
    {
        public Params1 @params;
        public int id = 0;
        public string jsonrpc = "2.0";
        public string method = "proxy_to_counterpartyd";
    }

    [Serializable]
    public class Params1
    {
        public string method = "create_send";
        public Params2 @params;
    }

    [Serializable]
    public class Params2
    {
        public bool allow_unconfirmed_inputs = false;
        public bool use_enhanced_send = true;
        public string encoding = "auto";
        public string pubkey = "公開鍵";
        public string source = "送り先";
        public string destination = "自分のアドレス";
        public string asset = "トークン名";
        public string memo = "メモ"
        public string memo_is_hex = "メモの16進数"
        public int quantity = 数量;
        public int fee_per_kb = 手数料;
        //public bool extended_tx_info = true; //何故かエラーになります
    }

    // 未署名Txの格納
    public class UnSignTxData
    {
        public string result;
    } 

    //未署名Txのリクエスト
    private void GetUnSignTxHex()
    {
        string url = "リクエスト先APIサーバー";

        MonapaApi monapaApi = new MonapaApi();
        string monapaJson = JsonUtility.ToJson(monapaApi);

        StartCoroutine(ReqUnSignTxHex(url , monapaJson));

        IEnumerator ReqUnSignTxHex(string url, string bodyJsonString)
        {
            UnityWebRequest postRequest = new UnityWebRequest(url, "POST");
            byte[] bodyRaw = Encoding.UTF8.GetBytes(bodyJsonString);
            postRequest.uploadHandler = (UploadHandler)new UploadHandlerRaw(bodyRaw);
            postRequest.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
            postRequest.SetRequestHeader("Content-Type", "application/json");

            yield return postRequest.SendWebRequest();

            UnSignTxData unSignTxData = new UnSignTxData();
            unSignTxData = JsonUtility.FromJson<UnSignTxData>(postRequest.downloadHandler.text);
            unSignTxHex = unSignTxData.result;

            if (postRequest.isHttpError || postRequest.isNetworkError)
            {
                Debug.Log(postRequest.error);
            }
            else
            {
                Debug.Log(postRequest.downloadHandler.text);
            }
    }
取得した未署名Txに署名する.cs
//上記の続きです。
private void SignRawTx()
    {
        //MonacoinのMainnetに接続
        NBitcoin.Network network = NBitcoin.Altcoins.Monacoin.Instance.Mainnet;

        //秘密鍵の読み込み
        var bitcoinPrivateKey = new BitcoinSecret("秘密鍵");
        network = bitcoinPrivateKey.Network;

        var transaction = Transaction.Create(network);
        transaction = Transaction.Parse(unSignTxHex, network);

        //署名する
        transaction.Sign(bitcoinPrivateKey, false);
        signedTxHex = transaction.ToHex();
    }

あとは signedTxHex を今までの要領で 「method = "broadcast_tx"」 を使いブロードキャストすればOK。返ってきたTxidをmpchain等で確認してみましょう。

なにか間違ってたらごめんなさい。

完成物

(その1)と同様です。

『monanosu』
https://monanosu.kotaroo.work/
※スマホの方は中央の"OK"を押して下さい。WiFi環境で遊びましょう。

「限定モナカード」が手に入る「宝箱探しゲーム」も先日追加しました。
https://twitter.com/7101kerokero/status/1204315335978958848?s=20

参考にしたもの

・『Counterpartyでアプリを簡単に作ろう!』
https://qiita.com/MissMonacoin/items/fedbf77f8b7aaefc037f

・『monacoind 不要の faucet を作ってみた (骨格だけ)』
https://qiita.com/cryptcoin-junkey/items/fc6d62c22d4444d98c45

・TwitterでアドバイスをくれるPさんありがとうございます。

おわりに

初めて作ったプログラムとの言い訳をしつつ、あらためて自分のコードを見るといろいろと突っ込みどころが多そうです。(アドバイス頂けたらうれしいです。)

記事の最初に「送金が出来るまでに自分は3か月くらいかかった」と書き後述する、と言いましたが疲れたのでその際に調べたことも含めて後日記事にします(たぶん)

それでは

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

DDD の練習してみた(C# 編)

本記事は ドメイン駆動設計#1 Advent Calendar 2019 18 日目の記事です。
元々私が参加登録していたわけではありませんが、空きが出ていたので、僭越ながら記事を書かせていただきます。

はじめに

最近 DDD が盛り上がっていますね。
私も数年前にエヴァンス本読んでちょくちょく勉強してたのですが、今年から色々カンファレンス行くようになって本格的に勉強始めました。
ということで、今回は個人的に DDD を練習してみた結果を書いてみたいと思います。
言語は普段使っている C# で書いています。

テーマは「備品予約」

練習しようと思ったきっかけは、「レガシーをぶっつぶせ。現場でDDD! 」に参加した際に購入した「もくもくモデリングの森を旅するチビドラゴンの軌跡」という本です。
サンプルのお題として会議室予約が挙げられており、会社の人と勉強やろうと思って話してたら、会社で使ってる備品の予約とかどう?となって、まずは自分でやってみようと思ったところが始まりです。

要件

まずは要件を以下のような感じでまとめました。
最初なので、そんなに複雑にはしませんでした。

  • 利用者は備品を予約できる。
    • 備品には USB, ポケットWifi, 携帯電話 がある。
    • 備品、予約時間、利用目的を指定して予約する。
    • 予約対象の備品がすでに同一時間帯に予約されている場合は予約できない。
  • 利用者は予約をキャンセルできる。
  • 利用者は予約を変更できる。

ドメインモデル

上記の要件から、ドメインオブジェクトを抽出していき、結果としてこんな感じになりました。

ドメインモデル.png

上記のように予約に備品を紐づけるか、備品に予約をぶら下げるか考えましたけど、予約が中心でしょ!という感じで前者にしました。笑

実装

上記のドメインモデルを実装していきます。

レイヤー分割

まずは DLL を以下のように分けました。
パッケージ.png

アプリケーション層とドメイン層がインフラ層に依存しないように、依存関係を逆転させました。
各層の中身は以下のようになっています。内容を少しづつ紹介していこうと思います。

Architecture.png

ドメイン層

Domain.png

  • Entity、ValueObject、DomainService などのドメインオブジェクトが格納されます。
  • 集約ルートごとに Repository のインターフェースが格納されます。
  • Entity では基本的にプリミティブ型は使わず ValueObject を使うようにしています。

ValueObject のコンストラクタで値のチェックをしています。

ReservationDateTime.cs
// 利用時間の ValueObject
public class ReservationDateTime : IValueObject
{
    public ReservationDateTime(DateTime start, DateTime end)
    {
        if (start.CompareTo(end) >= 0)
            throw new ArgumentException("終了日時は開始日時よりも後にしてください。");

        Start = start;
        End = end;
    }

    public DateTime Start { get; }
    public DateTime End { get; }
    // 以下省略
}
PurposeOfUse.cs
// 利用目的 の ValueObject
public class PurposeOfUse : IValueObject
{
    public PurposeOfUse(string value)
    {
        Assertion.ArgumentRange(value, 64, nameof(PurposeOfUse));
        Value = value;
    }

    public string Value { get; }

    // 以下省略
}

Entity は各フィールドのプロパティを用意し、private な Setter の中で値のチェックを行います。値を変更するときは SetXXX のような名称ではなく、実際のドメインで使われている名前を付けたメソッドを用意して呼び出せるようにしています。(英語のセンスがなくて微妙かもしれないが。)

Reservation.cs
public class Reservation : IEntity
{
    public Reservation(
        ReservationId id,
        AccountId accountId,
        EquipmentId equipmentId,
        ReservationDateTime reservationDateTime,
        PurposeOfUse purposeOfUse,
        ReservationStatus reservationStatus)
    {
        Id = id;
        AccountId = accountId;
        EquipmentId = equipmentId;
        ReservationDateTime = reservationDateTime;
        ReservationStatus = ReservationStatus.Reserved;
        PurposeOfUse = purposeOfUse;
        ReservationStatus = reservationStatus;
    }

    private ReservationId _id;
    public ReservationId Id
    {
        get { return _id; }
        private set
        {
            Assertion.ArgumentNotNull(value, nameof(Id));
            _id = value;
        }
    }

    private ReservationDateTime _reservationDateTime;
    public ReservationDateTime ReservationDateTime
    {
        get { return _reservationDateTime; }
        private set
        {
            Assertion.ArgumentNotNull(value, nameof(ReservationDateTime));
            _reservationDateTime = value;
        }
    }

    private PurposeOfUse _purposeOfUse;
    public PurposeOfUse PurposeOfUse
    {
        get { return _purposeOfUse; }
        private set
        {
            Assertion.ArgumentNotNull(value, nameof(PurposeOfUse));
            _purposeOfUse = value;
        }
    }

    
    
    

    public void ChangeReservationDateTime(ReservationDateTime reservationDateTime)
    {
        ReservationDateTime = reservationDateTime;
    }

    public void ChangePurposeOfUse(PurposeOfUse purposeOfUse)
    {
        PurposeOfUse = purposeOfUse;
    }

    public bool IsDupulicated(Reservation other)
    {
        Assertion.ArgumentNotNull(other, nameof(other));
        if (!EquipmentId.Equals(other.EquipmentId)) return false;
        if (!ReservationDateTime.IsRangeOverlapping(other.ReservationDateTime)) return false;
        return true;
    }
    // 以下省略
}

アプリケーション層

Application.png

  • アプリケーションサービスを格納します。
  • CQRS の考え方を利用して、Command と Query のアプリケーションサービスを分けています。ただし、DB は共通としています。(Command は ApplicationService、Query は QueryService という名前にしている。)
  • アプリケーションサービスでは Unit Of Work を利用したトランザクション管理を行っています。このレイヤーには、UnitOfWork のインターフェースの未定義し、実装はインフラ層で行っています。
  • QueryService では、直接 SQL 発行しておらず、インターフェース(IQuery)を定義して、SQL の発行はインフラ層で行っています。(QueryService を挟まずとも、IQueryService の実装をインフラ層で行っても構わないと思います。)
ReservationAppService.cs
public class ReservationAppService : IReservationAppService
{
    private readonly IUnitOfWork _unitOfWork;

    public ReservationAppService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
    }

    public void RegisterReservation(RegisterReservationRequest request)
    {
        _unitOfWork.Begin();
        try
        {
            _unitOfWork.ReservationRepository.Lock();
            var reservation = new Reservation(
                new ReservationId(),
                new AccountId(request.AccountId),
                new EquipmentId(request.EquipmentId),
                new ReservationDateTime(request.StartDateTime, request.EndDateTime),
                new PurposeOfUse(request.PurposeOfUse),
                ReservationStatus.Reserved);
            SaveReservation(reservation);
            _unitOfWork.Commit();
        }
        catch
        {
            _unitOfWork.Rollback();
            throw;
        }
    }

    public void ChangeReservationInfo(ChangeReservationInfoRequest request)
    {
        _unitOfWork.Begin();
        try
        {
            _unitOfWork.ReservationRepository.Lock();
            var reservation = FindReservationWithValidation(request.ReservationId);
            reservation.ChangeAccountOfUse(new AccountId(request.AccountId));
            reservation.ChangeEquipment(new EquipmentId(request.EquipmentId));
            reservation.ChangeReservationDateTime(new ReservationDateTime(request.StartDateTime, request.EndDateTime));
            reservation.ChangePurposeOfUse(new PurposeOfUse(request.PurposeOfUse));
            SaveReservation(reservation);
            _unitOfWork.Commit();
        }
        catch
        {
            _unitOfWork.Rollback();
            throw;
        }
    }

    public void CancelReservation(CancelReservationRequest request)
    {
        _unitOfWork.Begin();
        try
        {
            var reservation = FindReservationWithValidation(request.ReservationId);
            reservation.Cancel();
            _unitOfWork.ReservationRepository.Save(reservation);
            _unitOfWork.Commit();
        }
        catch
        {
            _unitOfWork.Rollback();
            throw;
        }
    }

    private Reservation FindReservationWithValidation(string reservationId)
    {
        var reservation = _unitOfWork.ReservationRepository.Find(new ReservationId(reservationId));
        if (reservation == null)
        {
            throw new ReservationNotFoundException();
        }
        return reservation;
    }

    private void SaveReservation(Reservation reservation)
    {
        var service = new ReservationService(_unitOfWork.ReservationRepository);
        if (service.IsDupulicatedReservation(reservation))
        {
            throw new ReservationDupulicationException();
        }
        _unitOfWork.ReservationRepository.Save(reservation);
    }
}

予約リスト画面や予約詳細画面には、予約のアカウント名や備品名を表示したいが、Entity を利用としようとすると、予約Entity、アカウントEntity、備品Entity のデータを取得しなければならず効率が悪いため、QueryService を利用して SQL で結合した結果を取得するようにします。(SQL の実装自体はインフラ層で行う。)

ReservationQueryService.cs
public class ReservationQueryService : IReservationQueryService
{
    private readonly IQueryFactory _queryFactory;

    public ReservationQueryService(IQueryFactory queryFactory)
    {
        _queryFactory = queryFactory ?? throw new ArgumentNullException(nameof(queryFactory));
    }

    public GetReservationDataResponse GetReservationData(GetReservationDataRequest request)
    {
        return new GetReservationDataResponse()
        {
            ReservationData = _queryFactory.ReservationDataQuery.FindReservationData(new ReservationId(request.ReservationId))
        };
    }

    public GetAllReservationListDataResponse GetAllReservationListData()
    {
        return new GetAllReservationListDataResponse()
        {
            ReservationListDataList = _queryFactory.ReservationDataQuery.FindAllReservationListData()
        };
    }
}

インフラ層

Infrastructure.png

  • Repository、Query の実装、UnitOfWork の実装を格納しています。
  • DB アクセスは Entity Framework、Dapper を利用しています。
ReservationRepository.cs
public class ReservationRepository : IReservationRepository
{
    private readonly MyDbContext _dbContext;

    public ReservationRepository(MyDbContext dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }

    public Reservation Find(ReservationId reservationId, ReservationStatus? reservationStatus = null)
    {
        IQueryable<RESERVATIONS> reservations = _dbContext.Reservations.Include(_ => _.reservations_status);
        if (reservationStatus != null)
        {
            reservations = reservations.Where(_ => _.reservations_status.status == (int)reservationStatus.Value);
        }
        var reservation = reservations.SingleOrDefault(_ => _.id == reservationId.Value);
        return Create(reservation);
    }

    public IEnumerable<Reservation> FindByEquipmentId(EquipmentId equipmentId, ReservationStatus? reservationStatus = null)
    {
        var reservations = _dbContext.Reservations.Include(_ => _.reservations_status).
            Where(_ => _.equipments_id == equipmentId.Value);
        if (reservationStatus != null)
        {
            reservations = reservations.Where(_ => _.reservations_status.status == (int)reservationStatus.Value);
        }
        return reservations.Select(_ => Create(_)).ToArray();
    }

    public void Save(Reservation entity)
    {
        var reservation = _dbContext.Reservations.Find(entity.Id.Value);
        if (reservation == null)
        {
            reservation = new RESERVATIONS();
            _dbContext.Reservations.Add(reservation);
        }
        reservation.id = entity.Id.Value;
        reservation.accounts_id = entity.AccountId.Value;
        reservation.equipments_id = entity.EquipmentId.Value;
        reservation.start_date_time = entity.ReservationDateTime.Start;
        reservation.end_date_time = entity.ReservationDateTime.End;
        reservation.purpose_of_use = entity.PurposeOfUse.Value;

        var reservationStatus = _dbContext.ReservationsStatus.Find(reservation.id);
        if (reservationStatus == null)
        {
            reservationStatus = new RESERVATIONS_STATUS();
            _dbContext.ReservationsStatus.Add(reservationStatus);
        }

        reservationStatus.reservations_id = reservation.id;
        reservationStatus.status = (int)entity.ReservationStatus;

        _dbContext.SaveChanges();
    }

    public void Lock()
    {
        _dbContext.QueryObjects<RESERVATIONS>("select * from reservations for update;");
    }

    private Reservation Create(RESERVATIONS reservation)
    {
        if (reservation == null)
            return null;

        return new Reservation(
            new ReservationId(reservation.id),
            new AccountId(reservation.accounts_id),
            new EquipmentId(reservation.equipments_id),
            new ReservationDateTime(reservation.start_date_time, reservation.end_date_time),
            new PurposeOfUse(reservation.purpose_of_use),
            (ReservationStatus)reservation.reservations_status.status);
    }
}
ReservationDataQuery.cs
public class ReservationDataQuery : IReservationDataQuery
{
    private readonly MyDbContext _dbContext;

    public ReservationDataQuery(MyDbContext dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }

    public ReservationData FindReservationData(ReservationId reservationId)
    {
        var reservation = _dbContext.Reservations.Find(reservationId.Value);

        return CreateReservationData(reservation);
    }

    public IEnumerable<ReservationListData> FindAllReservationListData()
    {
        return _dbContext.Reservations.
            Include(_ => _.accounts).
            Include(_ => _.equipments).
            Include(_ => _.reservations_status).
            Where(_ => _.reservations_status.status == (int)ReservationStatus.Reserved).
            Select(_ => CreateReservationListData(_)).ToArray();
    }

    public ReservationListData FindReservationListData(ReservationId reservationId)
    {
        var reservation = _dbContext.Reservations.
            Include(_ => _.accounts).
            Include(_ => _.equipments).
            Where(_ => _.id == reservationId.Value).
            SingleOrDefault();

        return CreateReservationListData(reservation);
    }

    private ReservationData CreateReservationData(RESERVATIONS reservation)
    {
        if (reservation == null)
            return null;

        return new ReservationData()
        {
            Id = reservation.id,
            AccountId = reservation.accounts.id,
            EquipmentId = reservation.equipments_id,
            StartDateTime = reservation.start_date_time,
            EndDateTime = reservation.end_date_time,
            PurposeOfUse = reservation.purpose_of_use,
        };
    }

    private ReservationListData CreateReservationListData(RESERVATIONS reservation)
    {
        if (reservation == null)
            return null;

        return new ReservationListData()
        {
            Id = reservation.id,
            AccountId = reservation.accounts.id,
            EquipmentId = reservation.equipments_id,
            StartDateTime = reservation.start_date_time,
            EndDateTime = reservation.end_date_time,
            PurposeOfUse = reservation.purpose_of_use,
            AccountName = reservation.accounts.account_name,
            EquipmentType = reservation.equipments.equipment_type,
            EquipmentName = reservation.equipments.equipment_name
        };
    }
}
UnitOfWork.cs
public class UnitOfWork : IUnitOfWork
{
    private readonly MyDbContext _dbContext;

    public UnitOfWork(MyDbContext dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }

    private IReservationRepository _reservationRepository;
    public IReservationRepository ReservationRepository
    {
        get
        {
            if (_reservationRepository == null) _reservationRepository = new ReservationRepository(_dbContext);
            return _reservationRepository;
        }
    }

    
    
    

    public void Begin()
    {
        _dbContext.Database.BeginTransaction();
    }

    public void Commit()
    {
        _dbContext.Database.CommitTransaction();
    }

    public void Rollback()
    {
        _dbContext.Database.RollbackTransaction();
    }

    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposed)
            return;

        if (disposing)
        {
            _dbContext.Dispose();
        }

        disposed = true;
    }
}

ビュー層

  • ASP .NET Core MVC の Web アプリケーションの入り口。
  • View、Controller を格納する。
  • DI(依存性の注入)を行う。

実装は、画面から入力されたデータを、ApplicationService、QueryService のメソッド引数の型に変換して、サービスのメソッドを呼び出す感じです。(投げやり)

まとめ

ということで、最後の方はだいぶ駆け足になりましたが、自分としてはなんとなく各レイヤーでどんな実装をすればよいか感触をつかめました。
モデリングがまだまだだなので、もう少し複雑なドメインを例にまたやってみようと思います。
あとは、Java、Spring を使ったサンプルも作っていきたいと思います。

ソースコードは以下に公開しています。
https://github.com/TakashiOnawa/EquipmentReservation

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

wpf ファイルを開いて、画像を表示させる

wpf ファイルを開いて、画像を表示させる

自分に備忘録用に書いています。
C#のコード

using System;
using System.Windows;
using System.Windows.Media.Imaging;
using Microsoft.Win32;

namespace File_Open_Project
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public string ImageFileName = null;
        public MainWindow()
        {
            InitializeComponent();
        }

        private void open_file_button_Click(object sender, RoutedEventArgs e)
        {
            OpenFileDialog openFileDialog = new OpenFileDialog();
            if (openFileDialog.ShowDialog() == true)
            {
                ImageFileName = openFileDialog.FileName;
            }

            BitmapImage bitmapImage = new BitmapImage(new Uri(ImageFileName));
            imageBox.Source = bitmapImage;
        }
    }
}

xamlのコード

<Window x:Class="File_Open_Project.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"
        xmlns:local="clr-namespace:File_Open_Project"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="400">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>


        <Menu>
            <MenuItem Header="ファイル">
                <MenuItem Header="開く" Name="open_file_button" Click="open_file_button_Click"/>
            </MenuItem>
        </Menu>


        <Image Source="Resources\noimage.png" Name="imageBox" Grid.Row="1"/>
    </Grid>
</Window>

しっかり、ビルドアクションをResourceにする。

1.PNG

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

Xamarin.Android と Azure Functions で全てがC#製のスマホアプリを作った話

この記事は C# Advent Calendar 2019 の19日目です。
前回は wasimaruさんによるASP.NET Core / ASP.NET Web API 2 Owin で Web API の自動テスト環境を整えるでした。

忙しい方のための概略

Xamarin.Androidを用いてスマホアプリを制作し、バックエンドに Azure Functions を採用することで全てをC#で完結させました
今回書き留めるのは、はこだてローカル勉強会"ゆるはこ"での登壇内容+αです。
登壇の際に用いたスライドはこちら(All C#なモバイル開発. / All C # Mobile Development.)

構成はこんな感じ(スライドより)
AllCSharpなモバイル開発.jpg

環境

  • Visual Studio 2019
  • Windows 10

作ったもの

地元にあるハンバーガーショップ「ラッキーピエロ」の特売情報をストレージから自動で出してくれるアプリ「ラピナビ」を制作しました。
個人開発で遊んでみたかっただけなので、一般公開はしていません。UIも実用性重視で、凝ったものではありません。

画面

Screenshot_20190923-125632.png

経緯

僕の住んでいる地域、北海道函館市には「ラッキーピエロ」という有名なハンバーガーショップがあります。テレビなどの媒体でよく紹介されているようなので、もしかするとご存じの方もいらっしゃるかもしれません。

このお店はよく割引キャンペーンなどを行うのですが、その情報をしっかりと捉えるのが結構大変なんです。
具体的には実施店舗が違ったり、対象商品が違ったり、実施期間が限られているなどなど…

キャンペーンに乗っかって最高の立ち回りをするためには覚えることが多いです。

そこで!情報をまとめたスマホアプリを作ってやろうという、自分による自分のためのアプリ開発を始めることにしました。

ALL_C#開発

自分はC#が大好きなので、どうせならすべての処理にC#を採用してやろうと考えました。

今回のスマホアプリ化に必要な機能は以下の通り

  • 日時を取得する
  • 定期割引日かどうかを判断する
  • 特別割引日かどうかをサーバーから情報を受け取り判断する

うん、これくらいなら簡単な実装でなんとかなりそうだ。

いろいろ考えた結果、特別割引日の情報を格納するストレージと、特別割引日の情報を送信するサーバが必要だと考えました。

「そういえば Azure に簡単にWebAPIを立てられるサービスがあったな」と考え、Azure Functionsにたどり着きます。

Azure Functions とは

1_XlE7tOMn_UzYKE0FolVnBA.png
Azure Functions は Microsoftが提供するクラウドサービスです。
最近流行りのサーバーレスアーキテクチャを採用しています。

従量課金制を採用しており、僕のような個人の開発者でも気軽に使用することができます。
(そして結構安かったりする)

またクラウドサービスであるため、オンプレミスほどサーバー管理を気にすることがなく気持ちよく開発を進めることができます。

使用言語はC#,Java,JavaScript,Pythonなどから選ぶことが可能です。
もちろん今回の開発ではC#を採用しました。

Azure Table Storageを使う

特別割引日かどうかをサーバーから情報を受け取り判断する

この機能を実現するには割引日の情報をサーバーに保存しておく必要があります。
そこで、DBのような働きをしてくれるのがこの Azure Table Storage です。

Azure Table Storage の作成とアクセスにはWindowsの場合 Microsoft Azure Storage Explorerが便利です。
Microsoft Azure Storage Explorerをインストールし、Table Storageを作成して、必要な特別割引情報を次の画像のようにぶち込みます。
image.png
これでデータ側の準備はできました。次はこのデータを吐き出すAPIの部分です。

Functionの作成

今回データの受け渡しにはJSONを用いますので、Azure FunctionsでJSONを吐き出すAPIを作成します。
プロジェクトの作成はとっても簡単。VisualStudioで「新しいプロジェクトの作成」で作成できます。
image.png
プロジェクトを作成したら、Azure Table Storageを使用するために"Microsoft.Azure.Webjobs.Extensions.Storage" を,JSON形式のデータを扱うために"Json.NET(Newtonsoft.Json)"NuGetで導入します。
image.png
image.png

そして、Azure Functions特有の記法でプログラムを記述。

内容は"POSTリクエストが送信されたときに、DBに入っている全ての要素をJSON化して送信する"というものです。

また、Azure FunctionsとAzure Table Storageのバインドはちょっとコツがあります。以下の文献を読むことをおすすめします。

Azure Functions における Azure Table Storage のバインド - GitHub - MicrosoftDocs/azure-docs.ja-jp

using System.Text;
using System.Net;
using System.Net.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Table;
using System.Collections.Generic;

namespace LpNaviFunc
{
    public static class GetInfo
    {
        [FunctionName("GetInfo")]
        public static HttpResponseMessage Run(
            //[Table(“LPInfo”)]などでデータとバインディングしている。
            //詳しい記法は Azure Functions の公式ドキュメントを読んでください。
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            [Table("LPInfo")] CloudTable cloudTable,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            //Azure Table Strageにアクセスし、データをリストに格納する。
            var querySegment = cloudTable.ExecuteQuerySegmentedAsync(new TableQuery<LPINFO>(), null);
            StringContent responseContent = null;
            var ls = new List<LPINFO>();

            foreach (LPINFO item in querySegment.Result)
            {
                ls.Add(item);
            }
            //すべての格納が終了した後に、Jsonファイルを生成している。
            responseContent = new StringContent(JsonConvert.SerializeObject(ls,Formatting.Indented),Encoding.UTF8,"application/json");
            //最後に生成したJsonファイルを返す。
            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = responseContent
            };
        }
    }

    //レコードを取得する用のクラス。
    //これを作ることによってListでの管理が可能になる。
    public class LPINFO : TableEntity
    {
        public string Shops { get; set; }
        public string Summary { get; set; }
    }

}

V2版を用いる際のちょっとした注意点

コードを記述していくに当たり多くの日本語記事で"Iqueryable"を使用するものが見受けられますが、Azure Functions V2からこの記法はできなくなっていました。

参考url

Functions v2: Can't bind Table to type System.Linq.IQueryable - GitHub

この情報は公式ドキュメントにも記述されていますので、公式ドキュメントを読む方ならそこまで詰まることはないと思います。(自分は精読せずに時間を捨ててしまった(公式ドキュメントはちゃんと読もう))

実際に起動してみるとAzure Functionsのマークが出てきて、おおっ!となります。

Xamarin.Androidを用いたモバイル側の実装

画面

バックエンド側はオッケーです。

フロント側はXamarin.Androidを用いてモバイル側を実装します。
そのために、「新しいプロジェクト」から「Androidアプリ(Xamarin)」を選択してプロジェクトを立てます。
image.png

画面はデザイナーやXamlファイルをよしなに編集してやって作成していきます。
image.png
ListViewを設置して、Listに格納したデータから情報を表示させる算段です。

ロジック

JSON形式のデータを扱うために"Json.NET(Newtonsoft.Json)"NuGetで導入します。

ここではボタンをクリックされたときに更新処理を行うようにしてあげたいので、お得意の"FindCiewById"をしてあげた後にイベントハンドラを与えます

//本物の実装ではFragmentを用いているのでちょっと形式が違うと思います…
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
    var view = inflater.Inflate(Resource.Layout.Suggest, container, false);

    var button = view.FindViewById<Button>(Resource.Id.updateButton/* ボタンのId */);
    button.Click += Clicked;
    return view;
}

private void Clicked(object sender, EventArgs e)
//ここにクリック時の処理を記述...
}

用いるデータ形式

本当はC#でよく用いられる Dictionary型を用いたかったのですが、この型だとうまくいきませんでした(JavaList < IDictionary< string, object>>()へのキャストでExceptionを吐いてしまいます)。
そこで、データ構造にはJavaListを採用することにしました。

//本当はDictionaryを用いたかったが、JavaList<IDictionary<string, object>>()へのキャストでExceptionを吐いてしまうためJavaDictionaryを用いた,
private JavaDictionary<string, string> dic = new JavaDictionary<string, string>();

Httpリクエストを送信してJSONを受け取る

Httpリクエストを送ってAzure Functions からJSONデータを受け取ります

JSON.NETを用いているので、デシリアライズも簡単で快適です。

//臨時キャンペーンの追加
//Azure functionsを利用している
//アクセスするURL
var url = "hogehoge";
using (var client = new HttpClient())
{
    try
    {
        //JSONを受け取って独自クラスにコンバートする
        var result = await client.GetStringAsync(url);
        var ls = JsonConvert.DeserializeObject<List<LPinfo>>(result);
        //データ格納処理
        for (int i = 0; i < ls.Count; i++)
        {
            dic[ls[i].Shops] = ls[i].Summary;
        }
    }
    catch (Exception)
    {
        //ここにエラー時処理
    }
}

そんなこんなでデータを受け取り、JavaListに格納することに成功しました。

あとはJavaListの内容をListViewに表示させるだけです。

//アダプター作成とセット
var lsView = View.FindViewById<ListView>(Resource.Id.outputView);
var arrayAdapter =
    new SimpleAdapter(this.Context,
    JavaListName,
    Android.Resource.Layout.SimpleListItem2,
    new string[] { "main", "sub" },
    new int[] { Android.Resource.Id.Text2, Android.Resource.Id.Text1 }
    );

lsView.Adapter = arrayAdapter;

こうして全てがC#製の自分用モバイルアプリの作成に成功しました。

得られた知見

  • Azure Functionsを用いると簡単にAPIを作成できる
    • そしてそこまでお金がかからない(かかっても10円くらい)
  • 全てをC#製にすると統一性があって開発がしやすい
  • データ構造の面で少し工夫をする必要があったりする
  • 全部C#でもしっかりとしたスマホアプリが作れる!!!!

今回紹介した全てがC#な開発事例、
そしてこれらの知見がどこかの誰かの役に立てば幸いです。

次回

次回のアドベントカレンダーは mei_9961 さんです!

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

CRI ADX2 LEのオーサリングツールAtomCraftがMacにも対応した話

CRIADXLEのオーサリングツール、AtomCraftとAtomViewerは長らくWindowsオンリーでしたが、今年に入ってついにMac版がリリースされました。
iOS向けにアプリを作っている方などは、Macしか持っていない方もおられると思いますが、これでデータの編集や確認が可能になりますね。
なお、商用のCRI ADX2でも同じくMac版がリリースされています。

CRI ADX2 LEとは

株式会社CRI・ミドルウェアから提供されている、オーディオミドルウェアです。
こちらからダウンロード可能です。
なお、利用については規約・利用契約内容をよくご確認ください。

インストール

DLしたものを解凍すると、tools/criatomex/macにインストーラーが入っています。こちらをダブルクリックでインストール。
アプリケーション/CRIWARE/CRI ADX2LE/Tools Ver.3.?にCriAtomCraft.appとCriAtomViewer.appが確認できます。
CriAtomCraftを開いてみましょう。

プロジェクトとデータを作る

「プロジェクトの新規作成」から任意のプロジェクトを作ります。
「マテリアルツリー」に入れたい音素材(wav)をドロップしましょう。
スクリーンショット 2019-12-19 18.24.02.png

キューを作成

今回は、1キューで3つのBGMが時間で切り替わる物を作ります。
ワークユニットツリー内でキューを作成し、トラックに入れた音素材を入れます。
適当なタイミングで、それぞれのボリュームをコントロールし、交互にフェードで再生されるようにしてみました。
スクリーンショット 2019-12-19 18.26.56.png

再生を確認

再生ボタンを押して、再生を確認してみます。

キューシートのビルド

ビルドしてUnityで再生できるようにしましょう。
「ビルド」→「Atomキューシートのビルド」を選択、今回はUnityで使いたいので、「追加出力」から「Unity Assets出力」をチェック。ビルドボタンを押します。
これで、必要なacfファイルとacbファイルが出来上がります。
スクリーンショット 2019-12-19 18.30.31.png

CriAtomViewerで確認

作られたacfとacbファイルをCriAtomViewerにドラッグ&ドロップします。
キュー名を選択して、再生してみましょう。
・・・ですが、自分のところでは暗号化された音声が流れてしまいました。これは自分の環境が商用CRI ADXを使っていて、かつ暗号化されたデータを再生したことがあるからかもしれません。作成済みのサンプルデータでも同様だったため、Unityでの再生を優先します。
スクリーンショット 2019-12-19 18.33.32.png

Unityで再生を確認

適当にサンプルシーンを改造して、再生してみます。
CRIWAREにいるCri Stomに、acfファイル、acbファイルを指定して・・・
スクリーンショット 2019-12-19 17.39.54.png

CRI AtomSourceにCueSheetとCueNameを指定。Play On Startなので、実行すれば再生されるはずです。
スクリーンショット 2019-12-19 17.39.37.png

実行してみます。

無事再生されました!

色々やってみよう

AtomCraftが使えると、ランダム再生やシーケンス再生などインタラクティブなオーディオ再生を色々使うことが可能です。
是非いろんな機能を試してみてください。

今回のサンプル作成にあたり、以下の素材を使わせていただきました。ありがとうございました。
楽曲素材:Music-Note.jp

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

ダミーSMTPサーバー(smtp4dev)

開発でメール送信のテストをするのに会社のメールサーバを使うのも良くないですし、間違ってお客さんのメールに送ってしまうという事故を防ぐために開発用で仮想プリンタみたいなSMTPサーバがないかと思い調べたらsmtp4devというダミーSMTPサーバーがあったので使い方を説明する。
.NET Coreで作られているので、Windows、Linux、Mac OS-X(および.NET Coreを利用)で動作します。

概要

今回はWindows10にインストールしてから動作テストまでを説明します。
古いバージョンでは、Windows GUIベース(ここ)のがありますが、今はexe実行して、ダミーSMTPサービスを立ち上げて、ブラウザでメールの受信状況を確認できます。
https://github.com/rnwood/smtp4dev

インストール

  1. GitHubからプログラムをダウンロード
    • リンクはGithubのreleasesからダウンロード(ここ) image.png
  • 今回はwindows10なのでx64ベースをダウンロード image.png
  1. 1.でダウンロードしたファイルを回答
  2. Rnwood.Smtp4dev.exeを実行
  3. ブラウザでサーバが立ち上がっているか確認

動作テスト

Powrshellの権限変更

PowerShellを実行したことがないPCであれば実行ポリシーによりメールの実行テストをすることができないので、下記のサイトで変更をする
PowerShell のスクリプトが実行できない場合の対処方法

動作テスト

1.Smtp4dev.exeを実行
ダウンロードしたファイルを解凍して、中にあるRnwood.Smtp4dev.exe を実行する。
image.png

image.png

2.PowerShell ISEを起動

3.下記のメール送信用のスクリプトをPowerShell ISEに貼り付け、実行ボタンを押す

param($count=1)

1..$count | %{ 
    write-host $_
    Send-MailMessage -To foo@bar.com -From from@from.com -Subject "Message $_" -SmtpServer localhost
}

image.png

4.Browserに下記のURLを貼り付けてメールが届いている確認できる
http://localhost:5000/
image.png

開発での使い方

また後で書きます

その他のダミーSMTPサーバ

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

ダミーSMTPサーバ(smtp4dev)

開発でメール送信のテストをするのに会社のメールサーバを使うのも良くないですし、間違ってお客さんのメールに送ってしまうという事故を防ぐためにダミーSMTPサーバがないかと思い調べたらsmtp4devというダミーSMTPサーバーがあったので使い方を説明する。
.NET Coreで作られているので、Windows、Linux、Mac OS-X(および.NET Coreを利用)で動作します。
image.png

概要

今回はWindows10にインストールしてから動作テストまでを説明します。
古いバージョンでは、Windows GUIベース(ここ)のがありますが、今回は、exe実行して、ダミーSMTPサービスを立ち上げて、ブラウザでメールの受信状況を確認できます。
https://github.com/rnwood/smtp4dev

インストール

  1. GitHubからプログラムをダウンロード
    • リンクはGithubのreleasesからダウンロード(ここ) image.png
  • 今回はwindows10なのでx64ベースをダウンロード image.png
  1. 1.でダウンロードしたファイルを回答
  2. Rnwood.Smtp4dev.exeを実行
  3. ブラウザでサーバが立ち上がっているか確認

動作テスト

Powrshellの権限変更

PowerShellを実行したことがないPCであれば実行ポリシーによりメールの実行テストをすることができないので、下記のサイトで変更をする
PowerShell のスクリプトが実行できない場合の対処方法

動作テスト

1.Smtp4dev.exeを実行
ダウンロードしたファイルを解凍して、中にあるRnwood.Smtp4dev.exe を実行する。
image.png

image.png

2.PowerShell ISEを起動

3.下記のメール送信用のスクリプトをPowerShell ISEに貼り付け、実行ボタンを押す

param($count=1)

1..$count | %{ 
    write-host $_
    Send-MailMessage -To foo@bar.com -From from@from.com -Subject "Message $_" -SmtpServer localhost
}

image.png

4.Browserに下記のURLを貼り付けてメールが届いている確認できる
http://localhost:5000/
image.png

開発での使い方

また後で書きます

その他のダミーSMTPサーバ

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

C#の初心者が、LINQの便利な機能をまとめてみた

はじめに

C#を始めてから、2,3ヵ月くらい経ちました。
C#を少し使えるようになったので、LINQという機能についてまとめてみようと思います。
すこし違う話が入ってるかもしれないので、ご了承ください。

まず、LINQとは

LINQとは、コレクション(配列など)の要素を処理するメソッドを集めたライブラリです。
使用するためには、using System.Linqを最初に書く必要があります。

便利な関数(メソッド)

  • Allメソッド
    配列内の要素すべてが、条件を満たすかを判定してくれます。
    本来なら、whileやforeachの中にif文を書いて、処理しなければならないことをAllメソッド一つでできるので便利です。

  • Anyメソッド
    Allメソッドと使い方は同じ感じです。
    Anyは、一つでも条件に合致するものがあれば、trueを返します。

  • Max,Minメソッド
    Maxは配列内の最大値、Minは配列内の最小値を求めてくれます。
    これらの計算は、LINQを使わずに書くと少し長めになりますがLINQのメソッドを使えば、1行で済みます!

  • Distinctメソッド
    配列内の重複要素を除いた配列を返してくれます。
    LINQを使わずに書くと、if文とforeachなどを使って、配列をもう一つ生成しないといけないので少し面倒です。このメソッドを使えば1行で済む!!

  • Sum,Averageメソッド
    Sumは配列の要素の合計、Averageは配列内の要素の平均を求めてくれます。
    実際に書いて比較してみました。

使用してないVer

 class Program
    {
        static void Main(string[] args)
        {
            List<int> val = new List<int> { 1, 2, 3, 4, 5 } ;
            int sum = 0;
            foreach(var i in val)
            {
                sum += i;
            }
            Console.WriteLine(sum);
        }
    }

使用したVer

class Program
    {
        static void Main(string[] args)
        {
            List<int> val = new List<int> { 1, 2, 3, 4, 5 } ;
            var sum = val.Sum();
            Console.WriteLine(sum);
        }
    }

LINQのメソッドを使ったほうがすっきりしてみえますよね!

LINQの利点

自分で書くと長くなる処理を関数でやってくれるから、プログラムの読みやすさも上がるし、自分で書かなきゃいけない処理が減るので便利。

最後に

みなさんもC#でぜひLINQ使ってみて下さい!
配列の処理が断然見やすくなります!!

参考

おすすめサイト

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

C#でアクセストークン(JWT)を発行して、Node.js(Express)で検証する

SC(非公式)Advent Calendar 2019 の19日目です。

はじめに

最近JWT周りのなんやかんやを触る機会が多いです。
別の言語での取り回しなんかもできるのが、JWTでの検証の良いところだと思います。

今回は.NetCore3.0で追加された 暗号化キーのインポート/エクスポートで、
RSAではなくECDsa(楕円暗号方式)で署名/検証しました。

サーバー構成としては以下になります。
image.png

実行環境

OS: mac OS Mojave 10.14.6
IDE: VS2019 for Mac community 8.3.6
.NetCore: 3.1.100
node: 10.14.1
npm: 6.4.1
クライアント: POSTMAN

余談ですが、MacでわざわざC#を触る人ってキチガイですよね~。
と、後輩に言われました。

秘密鍵・公開鍵の作成

以下のコマンドで楕円曲線暗号方式で秘密鍵と公開鍵を作成します。

ssh-keygen -t ecdsa -b 256 -m PEM -f jwtES256.key
openssl ec -in jwtES256.key -pubout -outform PEM -out jwtES256.key.pub

C#でIDProviderを作成

JWTを発行するC#のプロジェクトを立ち上げます。
必要なパッケージとして
Microsoft.AspNetCore.Authentication.JwtBearer
を追加しています。

# ワークフォルダ
mkdir JwtSample
cd JwtSample
# ソリューションの作成
dotnet new sln
# WebAPIテンプレートのプロジェクト作成
dotnet new webapi -o ./CSharpIDP
# ソリューションにプロジェクトを追加
dotnet sln add ./CSharpIDP
# JWTでの認証をするためにNugetパッケージを追加
dotnet add ./CSharpIDP package Microsoft.AspNetCore.Authentication.JwtBearer

いざJWTを生成

まずはAuthenticationControllerの全貌をさらします。

AuthenticationController.cs
using CSharpIDP.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;

namespace CSharpIDP.Controllers
{
  [ApiController]
  [Route("[controller]")]
  public class AuthenticationController : ControllerBase
  {
    private readonly ILogger<AuthenticationController> _logger;

    public AuthenticationController(ILogger<AuthenticationController> logger)
    {
      _logger = logger;
    }

    [HttpPost]
    public async Task<IActionResult> Token([FromBody] LoginModel model)
    {
      var tokenString = await AuthenticateAsync(model);
      if (tokenString != "")
      {
        return Ok(new { token = tokenString });
      }
      return Unauthorized();
    }

    private async Task<string> AuthenticateAsync([FromBody] LoginModel model)
    {
      _logger.LogInformation("AuthenticateAsync");

      var user = await FetchUserAsync(model.Email); // DB接続などを想定

      if (user.Email == model.Email && user.Password == model.Password)
      {
        var tokenString = GenerateToken(user);
        return tokenString;
      }
      return "";
    }

    private async Task<UserInfo> FetchUserAsync(string email)
    {
      _logger.LogInformation($"fetch user data by email={email}");
      return await Task.Run(() => new UserInfo
      {
        UserId = 888,
        UserName = "jwtSignningUser",
        Email = "aaa@gmail.com",
        Password = "password",
        Groups = new int[] { 1, 2, 3 }
      });
    }

    private string GenerateToken(UserInfo user)
    {
      var claims = new[] {
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(JwtRegisteredClaimNames.Sid, user.UserId.ToString()),
        new Claim(JwtRegisteredClaimNames.Sub, "JWT Sample for node.js"),
        new Claim(JwtRegisteredClaimNames.Email, user.Email)
      };

      var pemStr = System.IO.File.ReadAllText(@"./jwtES256.key");
      var der = StringUtil.ConvertX509PemToDer(pemStr);
      using var ecdsa = ECDsa.Create();
      ecdsa.ImportECPrivateKey(der, out _);
      var key = new ECDsaSecurityKey(ecdsa);
      var creds = new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256);
      var jwtHeader = new JwtHeader(creds);
      var jwtPayload = new JwtPayload(
        issuer: "https://localhost:5001/",
        audience: "https://localhost:3000/",
        claims: claims,
        notBefore: DateTime.Now,
        expires: DateTime.Now.AddMinutes(600),
        issuedAt: DateTime.Now
      );

      var token = new JwtSecurityToken(jwtHeader, jwtPayload);
      return new JwtSecurityTokenHandler().WriteToken(token);
    }
  }

  public class LoginModel
  {
    public string Email { get; set; } = "";
    public string Password { get; set; } = "";
  }

  public class UserInfo
  {
    public int UserId { get; set; }
    public string? UserName { get; set; }
    public string? Email { get; set; }
    public string? Password { get; set; }
    public int[]? Groups { get; set; }
  }
}

ではでは、GenerateTokenメソッドの解説をしていきます

JWTのスキーマ

JWTは、RFC7519で定義されているスキーマを持っていて、大きく以下の3種類のスキーマ定義があります。詳しくはここのサイトが大変参考になります。(JSON Web Token(JWT)のClaimについて)

  • Registered Claim Names
  • Public Claim Names
  • Private Claim Names

Registered Claim Names

Registered Claim Namesはあらかじめ決められた、「JWTならこれ持ってますよね」という定義です。

予約語 意味 役割
iss Issuer JWTの発行者。文字列かURIの形式
sub Subject JWTの用途。文字列かURIの形式
aud Audience JWTの利用者。文字列かURIの形式
exp Expiration Time JWTの失効する日時
nbf Not Before JWTが有効になる日時
iat Issued At JWTの発行日時
jti JWT ID JWTを一意な識別子。UUIDなどを入れるのが一般的

これらのスキーマ定義に則って、JwtPayloadクラスの設定をしているのが、以下の部分です。

      var claims = new[] {
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(JwtRegisteredClaimNames.Sid, user.UserId.ToString()),
        new Claim(JwtRegisteredClaimNames.Sub, "JWT Sample for node.js"),
        new Claim(JwtRegisteredClaimNames.Email, user.Email)
      };

// ... 略

      var jwtPayload = new JwtPayload(
        issuer: "https://localhost:5001/",
        audience: "http://localhost:3000/",
        claims: claims,
        notBefore: DateTime.Now,
        expires: DateTime.Now.AddMinutes(60),
        issuedAt: DateTime.Now
      );

現在から有効な、http://localhost:3000向けのJWTを発行しています。
有効期限は現在から1時間です。
実際のアプリではユーザーIDなどを入れると思いますので、Private Claim NamesとしてSid属性に入れています。

ECDsaでの署名

今回は、どの言語でも汎用的に使用できるように、opensslで秘密鍵と公開鍵のファイルを作成しました。
もちろんC#のプログラムからキーの生成を行うこともできますが、PEMファイルを読み込むとき少しハマったので、ご紹介。

// 秘密鍵ファイルの内容を取得
var pemStr = System.IO.File.ReadAllText(@"./jwtES256.key");
// PEM形式からbase64にデコード
var der = StringUtil.ConvertX509PemToDer(pemStr);
// ECDsaのインスタンス化
using var ecdsa = ECDsa.Create();
// der形式のデータをインポート
ecdsa.ImportECPrivateKey(der, out _);
// SecurityKeyインスタンス生成
var key = new ECDsaSecurityKey(ecdsa);

ECDsaのImportECPrivateKeyメソッドはこんな定義になっているので、
ファイルの余分な部分を削除して、base64デコードして渡してあげないとダメです。

image.png

ですので、Utilクラスでこんな泥くさいことをやっています。

    public static byte[] ConvertX509PemToDer(string pemContents)
    {
      var base64 = pemContents
          .Replace("-----BEGIN EC PRIVATE KEY-----", string.Empty)
          .Replace("-----END EC PRIVATE KEY-----", string.Empty)
          .Replace("\r\n", string.Empty)
          .Replace("\n", string.Empty); // Windowsだったらこの行は不要かも
      return Convert.FromBase64String(base64);
    }

メールアドレス・パスワードでトークンを取得

POSTMANからメールアドレスとパスワードでアクセストークンを取得します。

定義
URL https://localhost:5001/authentication
メソッド POST
ヘッダー Content-Type:application/json
BODY { "Email": "aaa@gmail.com", "Password": "password"}

image.png

DECsaの形式で署名されたJWTを取得できました。

Node.jsで検証サーバーを作成

つづいてはアクセストークンを検証するサーバーをNode.jsで作っていきます。
Expressのテンプレートを作成するexpress-generatorをグローバルインストールして、
適当なアプリを作成します。

npm install -g express-generator
# ワークディレクトリ
mkdir Express
cd Express
# verifyappという名前で作成
express verifyapp -e
cd verifyapp
# パッケージをインストール
npm i
# JWTを扱うためのパッケージもインストール
npm i jsonwebtoken
# サーバー起動
npm start

これでhttp://localhost:3000でサーバーが立つはずです。
app.jsは以下のコードを追加します。

app.js
// ... 略

+ var jwt = require("jsonwebtoken");
+ var fs = require('fs');

// ... 略

// ... 略

- app.use("/users", usersRouter);
+ app.use("/users", Authorize, usersRouter);

+ function Authorize(req, _, next) {
+   const authHeader = ParseAuthHeader(req.headers);
+   if (!authHeader) next(createError(401));
+   const token = authHeader.value;

+   const publicKey = fs.readFileSync("./jwtES256.key.pub", { encoding: "utf8" + });
+   const options = {
+     algorithms: ["ES256"] // 署名オプション
+   };
+   const decodedToken = jwt.verify(token, publicKey, options);
+   if (typeof decodedToken !== "object") next(createError(401));
+   req.token = decodedToken;
+   next();
+ }

+ function ParseAuthHeader(headers) {
+   const AUTH_HEADER = "authorization";
+   const regex = /(\S+)\s+(\S+)/;

+   if (!headers[AUTH_HEADER]) return undefined;
+   if (typeof headers[AUTH_HEADER] !== "string") return undefined;

+   const matches = headers[AUTH_HEADER].match(regex);
+   return matches && { scheme: matches[1], value: matches[2] };
+ }

// ... 略

module.exports = app;

Authorizeというミドルウェアを追加しています。
何をやっているか詳しく見ていくと、

app.js
function Authorize(req, _, next) {
  // リクエストヘッダーのauthorizationヘッダーからベアラートークンを取得
  const authHeader = ParseAuthHeader(req.headers);
  if (!authHeader) next(createError(401));
  const token = authHeader.value;

  // 公開鍵ファイルを読み込み
  const publicKey = fs.readFileSync("./jwtES256.key.pub", { encoding: "utf8" });
  const options = {
    algorithms: ["ES256"] // 署名アルゴリズムを指定
  };
  const decodedToken = jwt.verify(token, publicKey, options);
  if (typeof decodedToken !== "object") next(createError(401));
  // トークンからペイロードの情報が取れたら、reqにtokenとして保存
  req.token = decodedToken;
  next();
}

userRouterの先のuser.jsファイルはこんな感じになっています。

user.js
var express = require('express');
var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send(`${req.token.sub}さんからのリクエストです(${req.token.sid})`);
});

module.exports = router;

では先ほど取得したトークン情報をauthorizationヘッダーに載せてアクセスしてみます。
POSTMANでAuthorizationタブからBaerer Tokenを選択して、Tokenに先ほど取得したToken値を入れてGetでSendするだけです。

image.png

Tokenがなかった場合、きちんと401が返ってきます。

image.png

以上です。

C#での検証もためしてみたので、雑に載せときます。

Startup.cs
using CSharpIDP.Utils;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using System.IO;
using System.Security.Cryptography;

namespace CSharpIDP
{
  public class Startup
  {
    public Startup(IConfiguration configuration)
    {
      Configuration = configuration;
      Ecdsa = ECDsa.Create();
    }
    ~Startup()
    {
      Ecdsa.Dispose();
    }
    public IConfiguration Configuration { get; }
    private ECDsa Ecdsa { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
      var pemStr = File.ReadAllText(@"./jwtES256.key.pub");
      var der = StringUtil.ConvertPubKeyToDer(pemStr); // 秘密鍵と同じことやってます
      Ecdsa.ImportSubjectPublicKeyInfo(der, out _);
      services.AddAuthentication(options =>
      {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
      })
      .AddJwtBearer(options =>
      {
        options.TokenValidationParameters = new TokenValidationParameters()
        {
          ValidateIssuer = true,
          ValidIssuer = "https://localhost:5001/",
          ValidateIssuerSigningKey = true,
          IssuerSigningKey = new ECDsaSecurityKey(Ecdsa),
          ValidateAudience = false,
          ValidateLifetime = false,
        };
      });

      services.AddControllers();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
      if (env.IsDevelopment())
      {
        app.UseDeveloperExceptionPage();
      }

      app.UseHttpsRedirection();

      app.UseRouting();

      app.UseAuthentication(); // 追加
      app.UseAuthorization();

      app.UseEndpoints(endpoints =>
      {
        endpoints.MapControllers();
      });
    }
  }
}

これで、認可したいControllerに[Authorize]属性つければ、
認証のフィルターができるようになります。

参考

.NET Core 3.0 の新機能
Embracing nullable reference types
JWT Signing using ECDSA in .NET Core

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

C#でJWTを発行して、Node.jsで検証する簡単なお仕事です

SC(非公式)Advent Calendar 2019 の19日目です。

はじめに

最近JWT周りのなんやかんやを触る機会が多いです。
別の言語での取り回しなんかもできるのが、JWTでの検証の良いところだと思います。

今回は.NetCore3.0で追加された 暗号化キーのインポート/エクスポートで、
RSAではなくECDsa(楕円暗号方式)で署名/検証しました。

サーバー構成としては以下になります。
image.png

実行環境

OS: mac OS Mojave 10.14.6
IDE: VS2019 for Mac community 8.3.6
.NetCore: 3.1.100
node: 10.14.1
npm: 6.4.1
クライアント: POSTMAN

余談ですが、MacでわざわざC#を触る人ってキチガイですよね~。
と、後輩に言われました。

秘密鍵・公開鍵の作成

以下のコマンドで楕円曲線暗号方式で秘密鍵と公開鍵を作成します。

ssh-keygen -t ecdsa -b 256 -m PEM -f jwtES256.key
openssl ec -in jwtES256.key -pubout -outform PEM -out jwtES256.key.pub

C#でIDProviderを作成

JWTを発行するC#のプロジェクトを立ち上げます。
必要なパッケージとして
Microsoft.AspNetCore.Authentication.JwtBearer
を追加しています。

# ワークフォルダ
mkdir JwtSample
cd JwtSample
# ソリューションの作成
dotnet new sln
# WebAPIテンプレートのプロジェクト作成
dotnet new webapi -o ./CSharpIDP
# ソリューションにプロジェクトを追加
dotnet sln add ./CSharpIDP
# JWTでの認証をするためにNugetパッケージを追加
dotnet add ./CSharpIDP package Microsoft.AspNetCore.Authentication.JwtBearer

いざJWTを生成

まずはAuthenticationControllerを新しく作成します。
全貌がこちら。

AuthenticationController.cs
using CSharpIDP.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;

namespace CSharpIDP.Controllers
{
  [ApiController]
  [Route("[controller]")]
  public class AuthenticationController : ControllerBase
  {
    private readonly ILogger<AuthenticationController> _logger;

    public AuthenticationController(ILogger<AuthenticationController> logger)
    {
      _logger = logger;
    }

    [HttpPost]
    public async Task<IActionResult> Token([FromBody] LoginModel model)
    {
      var tokenString = await AuthenticateAsync(model);
      if (tokenString != "")
      {
        return Ok(new { token = tokenString });
      }
      return Unauthorized();
    }

    private async Task<string> AuthenticateAsync([FromBody] LoginModel model)
    {
      _logger.LogInformation("AuthenticateAsync");

      var user = await FetchUserAsync(model.Email); // DB接続などを想定

      if (user.Email == model.Email && user.Password == model.Password)
      {
        var tokenString = GenerateToken(user);
        return tokenString;
      }
      return "";
    }

    private async Task<UserInfo> FetchUserAsync(string email)
    {
      _logger.LogInformation($"fetch user data by email={email}");
      return await Task.Run(() => new UserInfo
      {
        UserId = 888,
        UserName = "jwtSignningUser",
        Email = "aaa@gmail.com",
        Password = "password",
        Groups = new int[] { 1, 2, 3 }
      });
    }

    private string GenerateToken(UserInfo user)
    {
      var claims = new[] {
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(JwtRegisteredClaimNames.Sid, user.UserId.ToString()),
        new Claim(JwtRegisteredClaimNames.Sub, "JWT Sample for node.js"),
        new Claim(JwtRegisteredClaimNames.Email, user.Email)
      };

      var pemStr = System.IO.File.ReadAllText(@"./jwtES256.key");
      var der = StringUtil.ConvertX509PemToDer(pemStr);
      using var ecdsa = ECDsa.Create();
      ecdsa.ImportECPrivateKey(der, out _);
      var key = new ECDsaSecurityKey(ecdsa);
      var creds = new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256);
      var jwtHeader = new JwtHeader(creds);
      var jwtPayload = new JwtPayload(
        issuer: "https://localhost:5001/",
        audience: "https://localhost:3000/",
        claims: claims,
        notBefore: DateTime.Now,
        expires: DateTime.Now.AddMinutes(600),
        issuedAt: DateTime.Now
      );

      var token = new JwtSecurityToken(jwtHeader, jwtPayload);
      return new JwtSecurityTokenHandler().WriteToken(token);
    }
  }

  public class LoginModel
  {
    public string Email { get; set; } = "";
    public string Password { get; set; } = "";
  }

  public class UserInfo
  {
    public int UserId { get; set; }
    public string? UserName { get; set; }
    public string? Email { get; set; }
    public string? Password { get; set; }
    public int[]? Groups { get; set; }
  }
}

ではでは、GenerateTokenメソッドの解説をしていきます

JWTのスキーマ

JWTは、RFC7519で定義されているスキーマを持っていて、大きく以下の3種類のスキーマ定義があります。詳しくはここのサイトが大変参考になります。(JSON Web Token(JWT)のClaimについて)

  • Registered Claim Names
  • Public Claim Names
  • Private Claim Names

Registered Claim Names

Registered Claim Namesはあらかじめ決められた、「JWTならこれ持ってますよね」という定義です。

予約語 意味 役割
iss Issuer JWTの発行者。文字列かURIの形式
sub Subject JWTの用途。文字列かURIの形式
aud Audience JWTの利用者。文字列かURIの形式
exp Expiration Time JWTの失効する日時
nbf Not Before JWTが有効になる日時
iat Issued At JWTの発行日時
jti JWT ID JWTを一意な識別子。UUIDなどを入れるのが一般的

これらのスキーマ定義に則って、JwtPayloadクラスの設定をしているのが、以下の部分です。

      var claims = new[] {
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(JwtRegisteredClaimNames.Sid, user.UserId.ToString()),
        new Claim(JwtRegisteredClaimNames.Sub, "JWT Sample for node.js"),
        new Claim(JwtRegisteredClaimNames.Email, user.Email)
      };

// ... 略

      var jwtPayload = new JwtPayload(
        issuer: "https://localhost:5001/",
        audience: "http://localhost:3000/",
        claims: claims,
        notBefore: DateTime.Now,
        expires: DateTime.Now.AddMinutes(60),
        issuedAt: DateTime.Now
      );

現在から有効な、http://localhost:3000向けのJWTを発行しています。
有効期限は現在から1時間です。
実際のアプリではユーザーIDなどを入れると思いますので、Private Claim NamesとしてSid属性に入れています。

ECDsaでの署名

今回は、どの言語でも汎用的に使用できるように、opensslで秘密鍵と公開鍵のファイルを作成しました。
もちろんC#のプログラムからキーの生成を行うこともできますが、PEMファイルを読み込むとき少しハマったので、ご紹介。

// 秘密鍵ファイルの内容を取得
var pemStr = System.IO.File.ReadAllText(@"./jwtES256.key");
// PEM形式からbase64にデコード
var der = StringUtil.ConvertX509PemToDer(pemStr);
// ECDsaのインスタンス化
using var ecdsa = ECDsa.Create();
// der形式のデータをインポート
ecdsa.ImportECPrivateKey(der, out _);
// SecurityKeyインスタンス生成
var key = new ECDsaSecurityKey(ecdsa);

ECDsaのImportECPrivateKeyメソッドはこんな定義になっているので、
ファイルの余分な部分を削除して、base64デコードして渡してあげないとダメです。

image.png

ですので、Utilクラスでこんな泥くさいことをやっています。

    public static byte[] ConvertX509PemToDer(string pemContents)
    {
      var base64 = pemContents
          .Replace("-----BEGIN EC PRIVATE KEY-----", string.Empty)
          .Replace("-----END EC PRIVATE KEY-----", string.Empty)
          .Replace("\r\n", string.Empty)
          .Replace("\n", string.Empty); // Windowsだったらこの行は不要かも
      return Convert.FromBase64String(base64);
    }

メールアドレス・パスワードでトークンを取得

POSTMANからメールアドレスとパスワードでアクセストークンを取得します。

定義
URL https://localhost:5001/authentication
メソッド POST
ヘッダー Content-Type:application/json
BODY { "Email": "aaa@gmail.com", "Password": "password"}

image.png

DECsaの形式で署名されたJWTを取得できました。

Node.jsで検証サーバーを作成

つづいてはアクセストークンを検証するサーバーをNode.jsで作っていきます。
Expressのテンプレートを作成するexpress-generatorをグローバルインストールして、
適当なアプリを作成します。

npm install -g express-generator
# ワークディレクトリ
mkdir Express
cd Express
# verifyappという名前で作成
express verifyapp -e
cd verifyapp
# パッケージをインストール
npm i
# JWTを扱うためのパッケージもインストール
npm i jsonwebtoken
# サーバー起動
npm start

これでhttp://localhost:3000でサーバーが立つはずです。
app.jsは以下のコードを追加します。

app.js
// ... 略

+ var jwt = require("jsonwebtoken");
+ var fs = require('fs');

// ... 略

// ... 略

- app.use("/users", usersRouter);
+ app.use("/users", Authorize, usersRouter);

+ function Authorize(req, _, next) {
+   const authHeader = ParseAuthHeader(req.headers);
+   if (!authHeader) next(createError(401));
+   const token = authHeader.value;

+   const publicKey = fs.readFileSync("./jwtES256.key.pub", { encoding: "utf8" + });
+   const options = {
+     algorithms: ["ES256"] // 署名オプション
+   };
+   const decodedToken = jwt.verify(token, publicKey, options);
+   if (typeof decodedToken !== "object") next(createError(401));
+   req.token = decodedToken;
+   next();
+ }

+ function ParseAuthHeader(headers) {
+   const AUTH_HEADER = "authorization";
+   const regex = /(\S+)\s+(\S+)/;

+   if (!headers[AUTH_HEADER]) return undefined;
+   if (typeof headers[AUTH_HEADER] !== "string") return undefined;

+   const matches = headers[AUTH_HEADER].match(regex);
+   return matches && { scheme: matches[1], value: matches[2] };
+ }

// ... 略

module.exports = app;

Authorizeというミドルウェアを追加しています。
何をやっているか詳しく見ていくと、

app.js
function Authorize(req, _, next) {
  // リクエストヘッダーのauthorizationヘッダーからベアラートークンを取得
  const authHeader = ParseAuthHeader(req.headers);
  if (!authHeader) next(createError(401));
  const token = authHeader.value;

  // 公開鍵ファイルを読み込み
  const publicKey = fs.readFileSync("./jwtES256.key.pub", { encoding: "utf8" });
  const options = {
    algorithms: ["ES256"] // 署名アルゴリズムを指定
  };
  const decodedToken = jwt.verify(token, publicKey, options);
  if (typeof decodedToken !== "object") next(createError(401));
  // トークンからペイロードの情報が取れたら、reqにtokenとして保存
  req.token = decodedToken;
  next();
}

userRouterの先のuser.jsファイルはこんな感じになっています。

user.js
var express = require('express');
var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send(`${req.token.sub}さんからのリクエストです(${req.token.sid})`);
});

module.exports = router;

では先ほど取得したトークン情報をauthorizationヘッダーに載せてアクセスしてみます。
POSTMANでAuthorizationタブからBaerer Tokenを選択して、Tokenに先ほど取得したToken値を入れてGetでSendするだけです。

image.png

Tokenがなかった場合、きちんと401が返ってきます。

image.png

以上です。

C#での検証もためしてみたので、雑に載せときます。

Startup.cs
using CSharpIDP.Utils;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using System.IO;
using System.Security.Cryptography;

namespace CSharpIDP
{
  public class Startup
  {
    public Startup(IConfiguration configuration)
    {
      Configuration = configuration;
      Ecdsa = ECDsa.Create();
    }
    ~Startup()
    {
      Ecdsa.Dispose();
    }
    public IConfiguration Configuration { get; }
    private ECDsa Ecdsa { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
      var pemStr = File.ReadAllText(@"./jwtES256.key.pub");
      var der = StringUtil.ConvertPubKeyToDer(pemStr); // 秘密鍵と同じことやってます
      Ecdsa.ImportSubjectPublicKeyInfo(der, out _);
      services.AddAuthentication(options =>
      {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
      })
      .AddJwtBearer(options =>
      {
        options.TokenValidationParameters = new TokenValidationParameters()
        {
          ValidateIssuer = true,
          ValidIssuer = "https://localhost:5001/",
          ValidateIssuerSigningKey = true,
          IssuerSigningKey = new ECDsaSecurityKey(Ecdsa),
          ValidateAudience = false,
          ValidateLifetime = false,
        };
      });

      services.AddControllers();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
      if (env.IsDevelopment())
      {
        app.UseDeveloperExceptionPage();
      }

      app.UseHttpsRedirection();

      app.UseRouting();

      app.UseAuthentication(); // 追加
      app.UseAuthorization();

      app.UseEndpoints(endpoints =>
      {
        endpoints.MapControllers();
      });
    }
  }
}

これで、認可したいControllerに[Authorize]属性つければ、
認証のフィルターができるようになります。

参考

.NET Core 3.0 の新機能
Embracing nullable reference types
JWT Signing using ECDSA in .NET Core

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

【Unity】Navigationを使って経路探索させる

概要

今回は以下のようなシンプルなプロジェクトを作り、Navigationの超基本的な使い方を学びます。

今回のプロジェクトでは、Unityちゃんがプレイヤー、赤いCubeが目的地になっています。

Qiita4.gif

【この記事の対象】 初中級程度の方

Navigationとは?

UnityにはNavigatonという機能が搭載されています。

Navigationとはシーンのジオメトリから生成されるナビゲーションメッシュを使用して、ゲームの世界を知的に歩くことのできる機能です。
(Unity公式リファレンスより:ナビゲーションと経路探索

今回はNavMeshAgentというコンポーネントを使ってNavigationを使っていきたいと思います。

1.GameObjectを配置

まずは任意の場所にプレイヤー、目的地、障害物、地面を配置します。

スクリーンショット 2019-12-14 11.54.43.png

今回はそれぞれのGameObjectに

種類      名前
プレイヤー(Unityちゃん) Player
目的地(Red) Destination
障害物(Green) Obstacle
地面(White) Ground

という名前をつけました!

2.NavMeshAgentをつける

次に今回の記事の要であるComponentのNavMeshAgentをつけていきます。

NavMeshAgentは経路探索をして、動いて欲しいObjectにアタッチします。
今回のプロジェクトではPlayerにアタッチします。

スクリーンショット 2019-12-14 10.51.22.png

3.Playerのスクリプトを書く

次にPlayerのスクリプトを書いていきます。

PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    NavMeshAgent Player_Nav;
    GameObject Destination;

    void Start()
    {
        //プレイヤーのNavMeshAgentを取得
        Player_Nav = GetComponent<NavMeshAgent>();
        //目的地のオブジェクトを取得
        Destination = GameObject.Find("Destination");
    }

    void Update()
    {
        //目的地を設定
        Player_Nav.SetDestination(Destination.transform.position);
    }
}

ポイントは

   1.名前空間using UnityEngine.AIを記入しているか
   2.PlayerのNavMeshAgentをきちんと取得できているか
   3.SetDestinationをきちんと書いている(目的地の指定ができている)

の3点です!!

4.NavigationWindowを開く

次にPlayerが行動できる範囲を指定するために、NavigationWindowを開きます。
navigationwindow.gif

5.移動範囲をBakeする

それではPlayerの移動範囲を指定していきましょう。

Ground、Obstacleを選択し、Inspectorビュー内のStaticの横にある三角をクリックし、Navigation Staticを選択します。

About Static.gif

次に歩かせる範囲と歩かせない範囲を指定します。

範囲の指定にはNavigationWindowのObjectのNavigation Areaの部分をいじっていきます。

Navigation Area 機能
Walkable 選択したエリアが歩けるようになる
Not Walkable 選択したエリアが歩けないようになる
Jump auto-generated、Off-Mesh Links の際に利用する(今回は使用しません)

スクリーンショット 2019-12-14 12.12.09.png

今回のプロジェクトにおいて歩かせる範囲はGround、歩かせない範囲はObstacleにあたるので、
GroundをWalkableに、ObstacleをNot Walkableに設定します。

以上の設定が完了したら、NavigationビューBakeの右下にあるBakeをクリックして移動範囲の指定を完了させます。

Bake3.gif

上の画像のように、移動範囲が青く表示されたら完了です!!!

6.動かしてみる

以上の設定が終わったらプレビューボタンを押してみましょう。

Preview.gif

PlayerのUnityちゃんが自動で経路探索し、指定範囲の上を歩きましたね!!
これでNavigation機能を使うことができました!!

最後に

いかがだったでしょうか?

Navigation機能はこんなに簡単に実装できてしまうんです。

次回の記事ではNavigation機能を使用して、動く障害物を避ける方法をご紹介したいと思います。

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

ビルド日時のリソース埋め込み

手段

参考:Displaying the build date
https://stackoverflow.com/questions/1600962/displaying-the-build-date/22163375

PEヘッダから取得する方法があるらしい。
しかし、細かい情報を入れたいこともありビルド前イベントを使って自前のリソースを追加する。

mkdir  "$(ProjectDir)\Resources\"
echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"

これだけだと、同名フォルダが存在する場合にエラーになる。

参考:
http://biglibox.hatenablog.jp/entry/2016/09/27/125444

mkdir  "$(ProjectDir)\Resources\" > NUL 2>&1
if ERRORLEVEL 1 cmd /c exit 0
echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"

と書いたが結局、素直にアセンブリのバージョンから求めた方がいいと思いました。

System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
System.Version version = asassembly.GetName().Version;

.NET core 3.0ではAssenblyInfo.csが無くなっていて、
プロジェクトのプロパティに組み込まれているのだが、
ワイルドカードが入力できなくて、BuildNumberのAuto increase設定に困っている。

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

[Copy & Paste] 都道府県

DropDownList 都道府県一覧

C# 都道府県一覧サーバー側で生成したい時用

public static List<SelectListItem> GetPrefecturesList(string prefecture)
{
  var prefectures = "北海道,青森県,岩手県,宮城県,秋田県,山形県,福島県,茨城県,栃木県,群馬県,埼玉県,千葉県,東京都,神奈川県,新潟県,富山県,石川県,福井県,山梨県,長野県,岐阜県,静岡県,愛知県,三重県,滋賀県,京都府,大阪府,兵庫県,奈良県,和歌山県,鳥取県,島根県,岡山県,広島県,山口県,徳島県,香川県,愛媛県,高知県,福岡県,佐賀県,長崎県,熊本県,大分県,宮崎県,鹿児島県,沖縄県";
  var listOfPrefectures = prefectures.Split(",");

  var selectListOfPrefectures = new List<SelectListItem>();

  foreach (var p in listOfPrefectures)
  {
    selectListOfPrefectures.Add(new SelectListItem()
    {
      Value = p,
      Text = p,
      Selected = (prefecture == p) ? true : false
    });
  }

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

コードを生成するコードを作ると人生が楽になる 【Unity】

シーン・サウンド・エフェクト等が追加された事で変数も新しく追加で用意する必要が出てきて
何回も決まりきった流れのコードを追記していく経験はありませんか

これはコードの自動生成で対策することが出来ます。

なにか難しそうに感じると思うのですが、とても単純でコード文を書いてファイル出力するだけで作れます

作り方

   private static string ASSETS_PATH = Application.dataPath + "/";
   private static string SOURCE_TEMPLATE =
    @"public class #SCRIPTNAME#
    {
    }";

    /// <summary>
    /// ソース出力
    /// </summary>
    /// <param name="fileName"></param>
    private static void CreateSource(string fileName)
    {
        // インポート開始
        AssetDatabase.StartAssetEditing();

        StreamWriter cs = new StreamWriter(ASSETS_PATH + fileName + ".cs", false, Encoding.UTF8);

        // 書き出すコードを組み立てる
        StringBuilder stringBuilder = new StringBuilder(SOURCE_TEMPLATE);

        // ※ここで用途に合わせ追記

        // ファイルにコードを書き込む
        cs.Write(stringBuilder);

        // ファイルを閉じる
        cs.Close();

        // インポート終了
        AssetDatabase.StopAssetEditing();

        // アセットインポートする
        AssetDatabase.Refresh(ImportAssetOptions.ImportRecursive);
    }

次回はこの仕組みを使って何か便利な仕組みを紹介できればと考えています。

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

C#エンジニアに贈るReact入門

この記事は React #2 Advent Calendar 2019 19日目の記事です。

はじめに

本職は、C#でWindows Formを使ったレガシーなアプリケーションを作っています。

ただ、レガシーなアプリを作っていると、Webフロントエンドは、進化が早くて、活発で、
なんかキラキラしている感じがして、うらやましくなってきます。

Reactは、趣味だけなのでまだまだですが、そんなC#エンジニア(以降、C#er)から見たReact入門を解説したいと思います。

同僚がReactを勉強したいと言い出したので、そのための資料でもあります。

React の Advent Calendar ということで他の方がReactのTipsを公開している中、
一人だけ空気を読まなくて申し訳ないです・・・。

なお、Webフロントエンド界隈は進化や世代交代が早いので、
この記事の賞味期限もせいぜい半年~1年くらいだと思っています。

要約

  • C#エンジニアがReactを始めるときは、Typescript、MobX、Material-UI、Class Componentを使うといいよ。
  • 解説をつけながらハンズオン的な感じでブログっぽいページを作るよ

想定読者

  • WindowsでC#を使ってWindows Formなアプリを作ってきた方
  • および、Webアプリは、CGIとかAjaxあたりで止まっている方

React基礎知識

そもそもReactって?

Webアプリのフロントエンドを構築するためのJavascriptライブラリです。
最近の流行として、htmlのページ遷移無しで、1ページ内で完結させようという流行(SPA、Single Page Application)があり、ReactはSPAを実現するためのフレームワークの1つです。
主な役割はhtmlのレンダリングであり、C#でいうと、WPFにおけるXAMLや、WindowsFormsの*.Designer.csに相当します。

Reactって何がうれしいの?

レガシーなWebサイトの場合、htmlでコンテンツを管理して、 見た目はCSS、何か動かすならJavascriptという感じだったと思います。

ただ、htmlで使えるのは決まったタグだけで、 部品化ができず、凝ったことをしようとすると作りこみが必要なわりに、それを他のページに展開するにはコピペが必要になってしまいます。

Reactを使うと、独自のHTMLタグを増やすことができるようになります。
さらに、単なるhtmlの部品ではなく、Javascriptもセットになっているので、
部品化によってリッチなUIを簡単に作ることができるようになります。

C#でいうなら、既定のButtonやLabelしか使えない状況から、UserControlが使えるようになったものです。

公開されている部品を組み合わせてもいいですし、もちろん自分で部品を作ることもできます。

例えば、次のようなブログのトップページをイメージしてもらうと、

Sample-Blog-Top.jpg

1つの記事のサマリーがAbstractContentという部品になっていれば、 それを並べればTopページが作れます。

<div className="TopPage">
    <AbstractContent title={title1} .... />
    <AbstractContent title={title2} .... />
    <AbstractContent title={title3} .... />
</div>

さらにAbstractContentも何かの部品を組み合わせているので 次のようなツリー構造になります。

Blog-Component-Tree.jpg

部品の例としては、material-uiのサイトを眺めるとこの部品化がイメージしやすいと思います。

Reactで部品はどうやって作るのか?

Reactの部品はこんな感じで作ります。

Reactコンポーネントの例
import React from "react";

interface BlogPartsProps {
  id: number;
  title: string;
  date: string;
  abstract: string;
}

class BlogParts extends React.Component<BlogPartsProps > {
  public render(): JSX.Element {
    return (
      <div>
        <a href={`/contents/${this.props.id}`}><p>{this.props.title} {this.props.date}</p></a>
        <p>{this.props.abstract}</p>
      </div>
    );
  }
}

export default BlogParts;

細かい説明は、他サイトを参考にしてもらったらよいと思いますが、
ポイントは次の2つです。

  • render関数では、JavascriptとHtmlが混ざった書き方をする。C#でいうと、RazorやT4テンプレートみたいなもの。
  • BlogPartsProps interfaceに定義されているフィールドが、外から渡されるパラメータ

BlogPartsPropsうんぬんは、C#のWindows Formで例えると、こんな感じで、外からセットされるプロパティに相当します。

C#でのPropsの例
class BlogParts : UserControl
{
  public int id {get;set;}
  public string title {get;set;}
  public string date {get;set;}
  public string abstractContent {get;set;}

  private void BlogParts_Load(object sender, EventArgs e)
  {
      _titleLabel.Text = this.Title;
      ・・・
  }
}

なお、Reactは、classで部品を作るパターン(Class Component)と、関数で部品を作るパターン(Functional Component)がありますが、C#erとしては、前者のほうがイメージしやすいです。
ただし、今の流行や推奨は後者のFunctional Componentです。React Hooksとかもこちらのようです。

今回は、C#er向けということでClass Componentでいきます。

Typescript

Typescriptは、Javascriptに型を導入した言語です。
ビルド(トランスクリプトというらしい)するとjavascriptになります。
ビルド結果のjavascriptは人が読むことは想定されていないようなので
ビルドするとバイナリになるC#に近い使用感です。
生のJavascriptは型がなく、なんとなくで実装できてしまいますが、
C#をやっている身からすると型が無いのが恐ろしいです。

ということで、Typescript、おすすめです。

言語的にはC#に近い言語っぽいけど、Javascriptの制約を引きずっているのと、独自進化しているようで、
知らない仕様がけっこうあります。

簡単なクラスを例にするとこんな感じです。

Typescriptの例
export class AppStore
{
    _id:number|undefined;  // 型はメンバーの後ろ、また、型に"|"を使ってORを表現できる。

    // メソッドはラムダ式を代入したメンバー。C#のメソッドっぽくも書けるけどこっちのほうがいいみたい
    public changeId = (id:number):void => {  
        this._id = id;
    }
    public getId = ():number => {
        if(this._id !== undefined)  //undefinedでないことをチェックしないとビルドが通らない
        {
            return this._id;
        }
        return 0;
    }
}

Windowsアプリでも問題になるけど内部状態はどう管理したらいいの?

React界隈でも問題になったようで、Flaxというアーキテクチャが生み出されています。

flux.jpg

参考: https://facebook.github.io/flux/

簡単に解説すると、(1)内部状態(Store)は1か所に集約する、
(2)GoFのCommandパターンで状態を更新する(ActionとDispatcher)、
(3)Viewは内部状態をただ表現するだけ、のようです。
この発想自体は、Windowsアプリケーションでも参考になりそうですね。

ただ、理念は良さそうですが、Flax実装の一つで一番人気のReduxは肌に合わなかったです。
単にReduxの実装がよくないだけかもしれませんし、
Commandパターンにありがちな、無駄に実装量が増えるあたりかもしれません。

代替策を調べた結果、もう少し簡易的な状態管理ライブラリである、MobXが良さそうに思えました。
MobXは、上の(1)~(3)の中では、(2)はあきらめて、
DIとGoFのObserverパターンを追加した感じです。
WPFにMVVMパターンがありますが、それに似ています。

すなわち、いろいろなViewModelは1か所で管理されて、Viewの定義に従ってDIされる、
ViewModelで状態が変わるとView側(React)に通知が来て、再描画される、という感じです。

MobX_Abstract.jpg

C#でMobXに似たような実装を書くとこんな感じ。

C#でのMobXっぽい実装イメージ
public class App: UserControl
{
    ViewModel _viewModel;

    [Inject("viewModel")]  //この属性を目印にDIされる
    public void SetViewModel(ViewModel viewModel)
    {
        _viewModel = viewModel;
        _viewModel.PropertiesUpdated += _viewModel_PropertiesUpdated;
    }

    _textBox1_TextChanged()
    {
        _viewModel.Text = _textBox1.Text;  //コントロールの変更はViewModelに伝える
    }

    _viewModel_PropertiesUpdated()  // ViewModelの変更から画面を更新する
    {
        // 描画更新
    }
}

public class ViewModel
{
    private string _text;
    public string Text {
        get { return _text; }
        set { _text = value; PropertiesUpdated?.Invoke();}  //プロパティの変更を通知する
    }
    public event Action PropertiesUpdated;
}

参考: https://mobx.js.org/README.html

Reactをとにかく使ってみたい

さて、基礎知識はここまでとして、実際に手を動かしてみましょう。
思ったより長くなったので、興味ない方は、以降は読み飛ばして、まとめに行ってもらってもいいです。

事前に必要なもの

環境は、C#er前提なのでWindowsです(ほかの環境でも動きそうですが確認していません)。
また、次のアプリをインストールしておく必要があります。

なお、React本体やTypescriptコンパイラは開発用フォルダにダウンロードされますので 上記以外ではOSを汚しません。(・・・はず)

開発環境

まずは開発環境を作ります。 次のコマンドで、create-react-appというReact開発環境のテンプレートを生成するツールをダウンロードします。なお、npmはC#でいうところのNugetです。

commandline
cd (適当な空フォルダー)
npm install create-react-app

ダウンロードしたモジュールはnode_modulesフォルダに入っており、実行モジュールはnode_modules/.binにあります。
それを使って、開発環境を生成します。
第一引数はフォルダ名(およびプロジェクト名)を、第二引数の--typescriptはJavascriptではなくTypescriptを使うことを指定しています。

commandline
node_modules\.bin\create-react-app test --typescript

出来上がった開発環境のフォルダに移動して、実行してみましょう。

commandline
cd test
npm start

初期の画面が表示されます。 npm start は、Visual Studioのデバッグの開始のようなもので、
自動的にhttpサーバーが起動し、ソースがビルドされて、Webページが表示されます。

React-init-screen.jpg

この状態で、ソースコードを一部変更してみます。
Visual Studio Code等のテキストエディタでsrc/App.tsxを編集します。

src/App.tsx
- Edit <code>src/App.tsx</code> and save to reload.
+ Hello World

ソースコードを変更すると自動でビルドされ、Webページも更新されます。

React-init-screen-Helloworld.jpg

終了は、コマンドプロンプトでCtrl+Cです。

開発環境のファイル構成はこんな感じです。
よく使うファイルは説明を付けました。

├ build         ・・・ビルド結果が入る。C#でいうとbin/Release
├ node_modules  ・・・ダウンロードしたパッケージがあります。C#でいうとpackages
├ public        ・・・この中身がビルド時にコピーされる。index.htmlとか入っている
├ src           ・・・ソースコードフォルダ
│  ├ App.css
│  ├ App.test.tsx ・・・App.tsxのユニットテスト。
│  ├ App.tsx      ・・・Web画面。ここをメインで編集します。C#でいうとForm1.Designer.cs
│  ├ index.css
│  ├ index.tsx    ・・・エントリーポイントです。C#でいうとProgram.csのmain関数
│  ├ logo.svg
│  ├ react-app-env.d.ts
│  ├ serviceWorker.ts
│  └ setupTests.ts
├ .gitignore
├ package.json      ・・・このプロジェクトの情報が記載されている。C#でいうと*.csproj
├ package-lock.json ・・・依存パッケージの情報。package.jsonにもあるけど、こっちがマスター?
├ README.md
└ tsconfig.json     ・・・Typescriptの設定。

開発環境を作りこむ

このままでも開発可能ですが、コードチェックの設定を行って、楽をしましょう。
C#でいうと、Visual Studioのコード分析か、Resharperですね。

必要なパッケージをダウンロードします。

commandline
npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-plugin-prettier prettier

コードチェックのルール用ファイルを作成します。

.eslintrc.json
{
    "extends": [
      "react-app",
      "eslint:recommended",
      "plugin:@typescript-eslint/recommended",
      "plugin:@typescript-eslint/eslint-recommended",
      "plugin:prettier/recommended",
      "prettier/@typescript-eslint"
    ],
    "plugins": [
      "@typescript-eslint"
    ],
    "parser": "@typescript-eslint/parser",
    "env": { "browser": true, "node": true, "es6": true },
    "parserOptions": {
      "sourceType": "module"
    },
    "rules": {

    }
  }

次のコマンドでソースコードのコードチェックが行われます。

commandline
node_modules/.bin/eslint src --ext .js,.jsx,.ts,.tsx
コードチェック結果
(WorkDir)\src\App.test.tsx
  1:19  error  Replace `'react'` with `"react"`                                        prettier/prettier
  2:24  error  Replace `'@testing-library/react'` with `"@testing-library/react"`      prettier/prettier
  3:17  error  Replace `'./App'` with `"./App"`                                        prettier/prettier

・・・

✖ 39 problems (33 errors, 6 warnings)
  33 errors and 0 warnings potentially fixable with the `--fix` option.

デフォルト状態では、大量にエラーが表示されます。

次に、Visual Studio Codeでの警告表示と保存時の自動Fixを設定します。
Visual Studio Codeの設定ファイルに以下を追加します。ファイルやフォルダがなければ新規作成してください。

.vscode\settings.json
{
    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {"language": "typescript", "autoFix": true },
        {"language": "typescriptreact", "autoFix": true }
      ]
}

これで、Visual Studio CodeでJavascriptやTypescriptのファイルを開いた場合に以下のように警告が表示され、
上書き保存時に自動修正できる箇所は修正されます。

VSCode-Warning.jpg

eslintの出力に記載があるように--fixをつけると自動Fixできますが、ファイル数も少ないので、
Visual Studio Codeで1つ1つファイルを開いては上書き保存していきます。

もう一度eslintを実行すると、自動解決できない警告だけになっています。
ここでは、気にしない方向で無視します。
なお、@typescript-eslint/explicit-function-return-typeを無視するようにeslintrc.jsonに記載するか、serviceWorkerの関数の戻り値をちゃんと宣言すれば、警告を消せます。

commandline
node_modules/.bin/eslint src --ext .js,.jsx,.ts,.tsx
コードチェック結果
(WorkDir)\src\serviceWorker.ts
   28:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   62:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   66:36  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   71:42  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  106:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  136:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type

✖ 6 problems (0 errors, 6 warnings)

毎回、上記のeslintコマンドを入れるのは面倒なので、mpn scriptにします。 (なおnode_modules/.bin のパスは、npmコマンドで実行すると解決されるので不要です)

package.json
-   "eject": "react-scripts eject"
+   "eject": "react-scripts eject", 
+   "lint": "eslint src --ext .js,.jsx,.ts,.tsx"

これで、npm run lintで実行できるようになります。

commandline
npm run lint
コードチェック結果
(WorkDir)\src\serviceWorker.ts
   28:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   62:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   66:36  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   71:42  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  106:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  136:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type

✖ 6 problems (0 errors, 6 warnings)

参考

Class Componentにする

App.tsxのデフォルトは、Functional Componentです。
上で話した通り、C#er的にはClass Componentのほうがわかりやすいので、Class Componentに変更します。

といっても、わりと機械的な作業で、 React.Componentを継承したclassを作って、
デフォルトで実装されていたApp関数をrender関数にします。

src/App.tsx(変更前)
const App: React.FC = () => {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Hello World</p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
};
src/App.tsx(変更後)
class App extends React.Component {
  public render = (): JSX.Element => {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>Hello World</p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  };
}

ポイントは以下です。

  • React.Componentを継承したclassを作ります。なお、Typescriptの継承はextendsです。
  • reander関数の戻り値はhtmlっぽい何かで、型的にはJSX.Elementになります。
    • classclassName にする(javascriptの構文と名前が被るので)
    • Htmlの属性は文字列以外は{}で囲ってJavascriptまたはTypescriptで記載する
    • styleは {{width:"50%"}} のように{}で囲われたJsonで書く

npm startしてみると、当然ですが、見た目は変わりません。

React-init-screen-Helloworld.jpg

参考: https://qiita.com/daikoncl/items/a3806d8a8bf35f086487

Material-UIを導入する

Reactのデフォルトでは、標準のhtmlタグしか使えません。 デザインセンス無しでそこそこイケてるWeb画面を作るために、既存の部品を使います。
ここでは、上で紹介した、Material-UIです。

まずパッケージをインストールします。

commandline
npm install @material-ui/core @material-ui/icons --save

そして、material-uiを使ったUIにApp.tsxをマルっと書き換えます。
Cardを使って、ブログのサマリーページ的な感じにしました。
Cardの使い方は、Material-UIの公式を参考に。
https://material-ui.com/components/cards/

src/App.tsx
import React from "react";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import Typography from "@material-ui/core/Typography";
import "./App.css";

class App extends React.Component {
  render(): JSX.Element {
    return (
      <div className="App">
        <Card style={{ width: 300, margin: 2 }}>
          <CardHeader
            action={
              <IconButton aria-label="settings">
                <MoreVertIcon />
              </IconButton>
            }
            title="タイトル"
            subheader="2019-12-17"
          />
          <CardContent>
            <Typography color="textSecondary" gutterBottom>
              アブストラクトです。
            </Typography>
          </CardContent>
        </Card>
        <Card style={{ width: 300, margin: 2 }}>
          <CardHeader
            action={
              <IconButton aria-label="settings">
                <MoreVertIcon />
              </IconButton>
            }
            title="タイトルその2"
            subheader="2019-12-17"
          />
          <CardContent>
            <Typography color="textSecondary" gutterBottom>
              アブストラクトその2です。
            </Typography>
          </CardContent>
        </Card>
      </div>
    );
  }
}

export default App;

npm startしてみると次のようになります。
いろいろスタイルは手抜きですが、影とかアイコンとかそれっぽくなりました。

React-blog-top.jpg

参考: https://material-ui.com/

データをサーバーから取得

現状、コンテンツはApp.tsxに埋め込まれています。
普通のサイトを考えるとJsonをサーバーから取得すると思います。

ざっくりJsonを想定するとこんな感じでしょうか。

[
  {
    "id": 1
    "title": "タイトル",
    "date": "2019-12-17",
    "abstract": "アブストラクトです。"
  },
  {
    "id": 2
    "title": "タイトルその2",
    "date": "2019-12-17",
    "abstract": "アブストラクトその2です。"
  }
]

Webページの組み込みは、いったん後回しとして、 サーバーからJsonを取得して、このデータをWebページに渡すViewModelを作り、 単体テストで動作確認します。

まずは、サーバーがないのでテスト時にHTTPリクエストを代替するパッケージfetch-mockを入れます。

commandline
npm install fetch-mock @types/fetch-mock node-fetch --save-dev

そして、ViewModelとユニットテスト用に2つのファイルを追加します。

src/AppStore.ts (ViewModelです)
export interface BlogSummary {
  id: number;
  title: string;
  date: string;
  abstract: string;
}

export class AppStore {
  data: BlogSummary[] = [];
  public fetchData = async (): Promise<void> => {
    const json = (await (
      await fetch("/api/summaries/")
    ).json()) as BlogSummary[];
    this.data = json;
  };
}
src/AppStore.test.ts (ユニットテストです)
import { AppStore } from "./AppStore";
import fetchMock from "fetch-mock";

const dummyData = [
  {
    id: 1,
    title: "タイトル",
    date: "2019-12-17",
    abstract: "アブストラクトです。"
  },
  {
    id: 2,
    title: "タイトルその2",
    date: "2019-12-17",
    abstract: "アブストラクトその2です。"
  }
];
fetchMock.get("/api/summaries/", dummyData);

test("fetchData test", () => {
  const target = new AppStore();
  target.fetchData().then(() => {
    expect(target.data).toEqual(dummyData);
  });
});

ついでに、上の変更ですでに壊れているので src/App.test.tsx を削除します。
(個人的にViewのテストは難しいわりにリターンが少ないので、あまりしません。
Typescriptで型はチェックできていますし。)

ユニットテストを実行してみましょう。

commandline
npm run test

成功しました。良さそうです。

ユニットテスト結果
 PASS  src/AppStore.test.ts
  √ fetchData test (5ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.573s
Ran all test suites related to changed files.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press Enter to trigger a test run.

参考: http://www.wheresrhys.co.uk/fetch-mock/

MobXを使ってViewModelとViewを結合する

ViewModel AppStore はルートである Index.tsxstores で管理し、App にインジェクトします。

Object-Diagram1.jpg

まずは必要なパッケージをインストールします。

commandline
npm install mobx mobx-react --save

そして、MobXはデコレーターを使用するのでTypescriptのオプションを変更します。

tsconfig.json
-    "jsx": "react"
+    "jsx": "react",
+    "experimentalDecorators": true
  },

index.tsxでストア(stores)を作り、 状態を管理する<Provider></Provider>Appを囲い、
Providerにはstoresを渡します。
これにより、Appの要求に従って、Providerから必要なViewModelを渡されます。

src/index.tsx
- ReactDOM.render(<App />, document.getElementById("root"));
+ import { AppStore } from "./AppStore";
+ import { Provider } from "mobx-react";
+ 
+ const stores = {
+   appStore: new AppStore()
+ };
+ 
+ ReactDOM.render(
+   <Provider {...stores}>
+     <App />
+   </Provider>,
+   document.getElementById("root")
+ );

ストアの appStore を受け取るように App で宣言します。

src/App.tsx(変更前)
class App extends React.Component {
src/App.tsx(変更後)
import { AppStore } from "./AppStore";
import { inject, observer } from "mobx-react";

interface AppProps {
  appStore?: AppStore;
}

@inject("appStore")
@observer
class App extends React.Component<AppProps> {

ポイントは以下です。

  • React部品で外からパラメータを受け取るにはPropsの仕組みを使います。
    • React.Componentのジェネリック型に、受け取るパラメータを宣言したインターフェイスAppPropsを指定します。
    • 今回はstoresで管理されているAppStoreを受け取るので、AppStore型のappStoreを宣言します。
  • @injectの引数およびAppPropsのプロパティには、受け取りたいstores内のプロパティ名appStoreを指定します。

これで、index.tsxstores内のappStoreAppに渡されます。

なお、当然ですが、見た目は変わりません。

React-blog-top.jpg

ViewModelを使ってWebページを更新する

上までで、AppにViewModelであるAppStoreが渡されたので、AppStoreの状態を使ってWebページを更新するようにします。 App.tsxをマルっと書き換えます。

src/App.tsx
@inject("appStore")
@observer
class App extends React.Component<AppProps> {
  // コンポーネントが構築される直前に呼ばれる
  public componentDidMount = (): void => {
    if (this.props.appStore === undefined) {
      return;
    }
    this.props.appStore.fetchData();
  }

  render(): JSX.Element {
    // appStoreはundefindedである場合があるので、その時は何も表示しない
    if (this.props.appStore === undefined) {
      return <div />;
    }

    // this.props.appStore.dataに従ってWebページを構築する
    return (
      <div className="App">
        {this.props.appStore.data.map(data => {
          return (
            <Card key={data.id} style={{ width: 300, margin: 2 }}>
              <CardHeader
                action={
                  <IconButton aria-label="settings">
                    <MoreVertIcon />
                  </IconButton>
                }
                title={data.title}
                subheader={data.date}
              />
              <CardContent>
                <Typography color="textSecondary" gutterBottom>
                  {data.abstract}
                </Typography>
              </CardContent>
            </Card>
          );
        })}
      </div>
    );
  }
}

ポイント

  • componentDidMount() はこのコンポーネントが構築される前に呼ばれます。
    • C#でいうところの、Form.Loadイベントです。
    • ここでは、サーバーからデータを取得するため、 appStore.fetchData() を呼び出します。
  • render() では、 this.props.appStore.data に従ってWebページを構築します。
    • {}で囲うとTypescriptを記述でき、returnでJSX.Elementを返します。

MobXは、プロパティが変更されたことを検知して、Viewを再描画させる機能を持っています。
これを使って、データが取得されたときにViewを更新します。
そのために AppStore のメソッドとプロパティに属性を付けます。

src/AppStore.ts
+ import { action, observable } from "mobx";


 export class AppStore {
+  @observable
   data: BlogSummary[] = [];
+  @action
   public fetchData = async (): Promise<void> => {
 }

これでサーバーデータに応じた画面を作ることができましたが、
このままでは、サーバーは存在しないのでエラーになります。
ユニットテスト用のfetch-mockを流用してダミーデータを使うようにします。

src/index.tsx
+ import fetchMock from "fetch-mock";
+ 
+ const dummyData = [
+   {
+     id: 1,
+     title: "[サーバー]タイトル",
+     date: "2019-12-17",
+     abstract: "アブストラクトです。"
+   },
+   {
+     id: 2,
+     title: "[サーバー]タイトルその2",
+     date: "2019-12-17",
+     abstract: "アブストラクトその2です。"
+   }
+ ];
+ fetchMock.get("/api/summaries/", dummyData);

  const stores = {
    appStore: new AppStore()
  };

これで、 npm start すると以下のようになります。

React-blog-top-json.jpg

dummyDataをいろいろ変えてみるとWebページが変わると思います。

部品を作る

Reactの部品化の練習のためにTOPページのサマリー要素部分を部品化してみます。

Object-Diagram2.jpg

上と同じようにReactコンポーネントのclassを作って、
renderでは、App.tsxで部品化したい部分を返すようにします。
必要なパラメータはPropsで上から渡してもらいます。
なお、Storesで状態管理するのはページごとで、部品ではViewModelは使わないほうが良いようです。

src/SummaryItem.tsx(新規作成)
import React from "react";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import Typography from "@material-ui/core/Typography";
import { Link } from "react-router-dom";

interface SummaryItemProps {
  title: string;
  date: string;
  abstract: string;
  url: string;
}

class SummaryItem extends React.Component<SummaryItemProps> {
  render(): JSX.Element {
    return (
      <Card style={{ width: 300, margin: 2 }}>
        <CardHeader
          action={
            <IconButton aria-label="settings">
              <MoreVertIcon />
            </IconButton>
          }
          title={<Link to={this.props.url}>{this.props.title}</Link>}
          subheader={this.props.date}
        />
        <CardContent>
          <Typography color="textSecondary" gutterBottom>
            {this.props.abstract}
          </Typography>
        </CardContent>
      </Card>
    );
  }
}

export default SummaryItem;

App.tsxのほうは、上で作った部品を使うように書き換えます。
なお、部品の参照は、importです。
また、不要になったimportも消しておきましょう。

src/App.tsx
- import Card from "@material-ui/core/Card";
- import CardHeader from "@material-ui/core/CardHeader";
- import CardContent from "@material-ui/core/CardContent";
- import IconButton from "@material-ui/core/IconButton";
- import MoreVertIcon from "@material-ui/icons/MoreVert";
- import Typography from "@material-ui/core/Typography";
+ import SummaryItem from "./SummaryItem";


-            <Card key={data.id} style={{ width: 300, margin: 2 }}>
-              <CardHeader
-                action={
-                  <IconButton aria-label="settings">
-                    <MoreVertIcon />
-                  </IconButton>
-                }
-                title={data.title}
-                subheader={data.date}
-              />
-              <CardContent>
-                <Typography color="textSecondary" gutterBottom>
-                  {data.abstract}
-                </Typography>
-              </CardContent>
-            </Card>
+            <SummaryItem
+              key={data.id}
+              title={data.title}
+              date={data.date}
+              abstract={data.abstract}
+            />

相変わらず見た目は変わりませんが、
サマリー部分の部品化ができました。

React-blog-top-json.jpg

なお、この部品は再利用しないので、部品化はあまり意味はありません。

ページ遷移を作る

SPAといえども、普通のHtmlのようにページ遷移がしたいことが多いです。
概要←→詳細 で行き来することもありますし、1ページいろいろ押し込むと使いにくいですし。

ここでは、React-Routerを使ってページ遷移を作ります。

まずページ遷移があるということはURLを設計しないといけません。
簡単ですが、以下のようにします。

  • http://~/         → トップページ(サマリーのページ)
  • http://~/contents/(Id)  → 詳細ページ(Idは数字)

では作ってみましょう。

まず、必要なパッケージを取得します。

commandline
npm install react-router-dom @types/react-router-dom  --save

次にTOPページから詳細ページへのリンクを作ります。
React-Routerでのリンクは<Link to="url">ラベル</Link>を使いますので、
タイトル部分をリンクに置き換えます。
URLはPropsに追加して、親ページから渡してもらうことにします。

src/SummaryItem.tsx
  import Typography from "@material-ui/core/Typography";
+ import { Link } from "react-router-dom";

  interface SummaryItemProps {
    title: string;
    date: string;
    abstract: string;
+   url: string;
  }

             </IconButton>
           }
-          title={this.props.title}
+          title={<Link to={this.props.url}>{this.props.title}</Link>}

親ページでは、URLを渡すように設定します。

src/App.tsx
              <SummaryItem
                key={data.id}
+               url={`./contents/${data.id}`}
                title={data.title}
                date={data.date}
                abstract={data.abstract}

これでトップページからのリンクはできました。
では、詳細ページを作っていきます。

作り方は、App.tsxと同じでページ(ContentPage.tsx)とViewModel(ContentPageStore.ts)を作って
MobXで連携させます。

Object-Diagram3.jpg

src/ContentPage.tsx (新規作成)
import React from "react";
import { RouteComponentProps } from "react-router-dom";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import Typography from "@material-ui/core/Typography";
import { ContentPageStore } from "./ContentPageStore";
import { inject, observer } from "mobx-react";

interface ContentPageProps extends RouteComponentProps<{ id: string }> {
  contentPageStore?: ContentPageStore;
}

@inject("contentPageStore")
@observer
class ContentPage extends React.Component<ContentPageProps> {
  public componentDidMount = (): void => {
    if (this.props.contentPageStore === undefined) {
      return;
    }
    const id = parseInt(this.props.match.params.id, 10);
    this.props.contentPageStore.fetchData(id);
  };

  render(): JSX.Element {
    if (this.props.contentPageStore === undefined) {
      return <div />;
    }
    const store = this.props.contentPageStore;
    return (
      <div className="App">
        <Card style={{ width: 300, margin: 2 }}>
          <CardHeader title={store.data.title} subheader={store.data.date} />
          <CardContent>
            <Typography color="textSecondary" gutterBottom>
              {store.data.content}
            </Typography>
          </CardContent>
        </Card>
      </div>
    );
  }
}

export default ContentPage;
src/ContentPageStore.ts (新規作成)
import { action, observable } from "mobx";

interface BlogContent {
  id: number;
  title: string;
  date: string;
  content: string;
}

export class ContentPageStore {
  @observable
  data: BlogContent = {
    id: 0,
    title: "",
    date: "",
    content: ""
  };

  @action
  public fetchData = async (id: number): Promise<void> => {
    const json = (await (
      await fetch(`/api/contents/${id}`)
    ).json()) as BlogContent;
    this.data = json;
  };
}

React-Routerの本番はここからです。
今までindex.tsx<App/>と直接Reactコンポーネントを指定していたところを、
<BrowserRouter>で囲って、<Route/>を並べていきます。
<Route>component=にReactコンポーネントを、path=にURLを指定します。

また、ViewModelもStoresに追加します。

src/index.tsx
  import fetchMock from "fetch-mock";
+ import ContentPage from "./ContentPage";
+ import { ContentPageStore } from "./ContentPageStore";
+ import { BrowserRouter, Route } from "react-router-dom";


  const stores = {
-   appStore: new AppStore()
+   appStore: new AppStore(),
+   contentPageStore: new ContentPageStore()
  };
  ReactDOM.render(
    <Provider {...stores}>
-     <App />
+     <BrowserRouter basename={"/"}>
+       <Route exact={true} path="/" component={App} />
+       <Route exact={true} path="/contents/:id" component={ContentPage} />
+     </BrowserRouter>
    </Provider>,
    document.getElementById("root")
  );

これで、/の場合はAppのページが、/contents/(id)の場合は、ContentPageのページが表示されるようになります。
URLのルートURLは、<BrowserRouter basename=に指定します。

昔ながらのCGIをやっていた方は、URLが違うなら別のhtmlが返ってくるのでは?という気がしますが、
SPAの場合は、バックエンド側で同じindex.htmlを返すように設定するようです。

一応これで完成なのですが、相変わらずバックエンドがないので
ダミーデータを返すようにします。

src/index.tsx
-     abstract: "アブストラクトです。"
+     abstract: "アブストラクトです。",
+     content: "これは本文です。"
    },
    {
      id: 2,
      title: "[サーバー]タイトルその2",
      date: "2019-12-17",
-     abstract: "アブストラクトその2です。"
+     abstract: "アブストラクトその2です。",
+     content: "これは本文です。"
    }
  ];
  fetchMock.get("/api/summaries/", dummyData);
+ fetchMock.get("/api/contents/1", dummyData[0]);
+ fetchMock.get("/api/contents/2", dummyData[1]);

以上で、簡単ですがブログっぽいページができました。
TOPページからリンクをたどると、詳細ページに飛びます。
URLも変化していますので、F5を押しても元ページに戻ったりしません。

React-blog-top-withLink.jpg

React-blog-content.jpg

ビルドしてデプロイ可能にする

ある程度アプリができたら、ビルドしてデプロイできるようにします。
次のコマンドを実行すると、buildフォルダに成果物が出来上がります。

commandline
npm run build

その他細かいTips

多言語対応は?

Javascript用にいくつかライブラリがあるようです。

基本的には、言語ごとにJsonを用意して、キーとなる文字列を指定すると、
実行時には言語に応じたJsonから文字列が取れるようです。

リソースの定義
{
  "buttons":{
      "new":"新規投稿"
      "edit":"編集"
  }
}
多言語リソースを使う例
   <Button label={i18next.t('buttons.new')} />

C#のResources.resxと比較すると、型がないので、インテリセンスが使えない分、ちょっと面倒です。

以前やったときに、Typescript+Jsonスキーマを使うといい感じに改善できそうだったので、
また記事を書くかもしれません。

サーバーサイドはどうする?

Node.jsもいいですが、C#erなら、過去のC#の資産を使えるASP.NET Coreを使いたいです。

Visual Studioで[プロジェクトの新規作成]-[ASP.NET Core Webアプリケーション]→[React.js]で
テンプレートを作って、ClientApp以下を上と同じ手順で作ったReact開発環境で置き換えれば、
いいようです。

普通にF5実行で、Reactのフロントエンド+C#のバックエンドで起動しました。
なお、初回はビルドが走るのか重いようです。

問題は、ASP.NET Coreを動かすサーバーを用意できるかどうか。

ReactとWindows Formで開発速度はどれくらい違う?

htmlが不慣れとかいろいろ要素がありますが、
私の場合、Windows Formで作るほうが3倍くらい速いです。

C# ←→ Typescriptの変換も面倒ですし、
画面レイアウトの作りこみも大変です。
というか、C#を振り返ると、Visual StudioのWindows Formのデザイナーはよくできているなぁと思います。
(壊れやすいのが難点ですが)

まとめ

割とがっつりReactの入門記事になってしまいましたが、
普段C#を使っている方の視点で、解説したつもりです。
Reactを使ってSPAを始めるための一通りを説明したつもりなので、
あとは応用を学んでいけば、そこそこのプロダクトが作れるのではないでしょうか。

なお、上で作ったコードは、一応GitHubに公開しておきます。
(この記事の賞味期限が有効なあたりまでは残しておきます)
https://github.com/banban525/react-example-2019

自分で打ち込むのが面倒な方はそちらを見てもらってもよいと思います。
個人的には写経はおすすめですが。

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

Xamarin C# 非同期関数内で起きたExceptionをthrowする

WindowsBaseパッケージに入ってる
System.Windows.ThreadingのDispatcherを使えば、
別のスレッド上でからメインスレッドでExceptionを起こすことができる。

非同期関数~~~
System.Windows.Threading.Dispatcher.CurrentDispatcher.Invoke(() =>
{
   throw new Exception("メインスレッドでExceptionが起こる");
});
~~~

キャッチするにはApplication.TheradExceptionにイベントを設定しておくと、
ハンドリングされなかったExceptionが飛んでくる

System.Windows.Forms.Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(this.OnThreadException);

private void OnThreadException(object sender, ThreadExceptionEventArgs e) {
}

Xamarin.Androidは以下でできる
Exception発生

非同期関数~~~~
Xamarin.Forms.Device.BeginInvokeOnMainThread(() =>
{
     throw new Exception("メインスレッドでExceptionが起こる");
});
~~~

キャッチ

AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(OnThreadException);

androidはunhandledExceptionイベントが走った時点でアプリが強制終了前提なので、拾ってもログ仕込むくらいのことしかできない
AndroidEnvironment.UnhandledExceptionRaiserを使えばアプリが落ちないとの情報もありましたが、だめでした。
https://qiita.com/ta-yamaoka/items/e374921d642e9ac84b3b
http://atamo-dev.hatenablog.com/entry/2016/10/30/174556

公式に回答ありました。
https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.unhandledexception?redirectedfrom=MSDN&view=netframework-4.8
~~~~~~~~~~~~~~~~~~~~~~~~~
NET Frameworkバージョン1.0および1.1では、メインアプリケーションスレッド以外のスレッドで発生する未処理の例外はランタイムによってキャッチされるため、アプリケーションは終了しません。したがって、アプリケーションを終了せずにUnhandledExceptionイベントが発生する可能性があります。.NET Frameworkバージョン2.0から、子スレッドの未処理の例外のこのバックストップは削除されました。そのようなサイレント障害の累積的な影響には、パフォーマンスの低下、破損したデータ、ロックアップが含まれていたためです。ランタイムが終了しないケースのリストなど、詳細については、「マネージスレッドの例外」を参照してください。
~~~~~~~~~~~~~~~~~~~~~~~~~~~

.Net Framework2.0から強制終了するようにしたみたいですね。

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

一日で俺の嫁を作る方法 ~一日で自分の好きな声・キャラ・見た目でおしゃべりしてくれるAIキャラクターを作ろう~

はじめに

いくつかのAPIを組み合わせることで、一日で自分の好きな声・キャラ・見た目でおしゃべりできるAIキャラクターを作りました。この記事を読めば同じ感じで誰でも一日で俺の嫁を作ることができるようになります。
また今回はVR環境版とスマートフォン環境版の二種類を作成しました。
これらのアプリとUnity Projectは以下のgithubで公開しています。
軽く試してみたい人は作り方の6に従って、AgentCraftでAIの会話例を作成して、それをSebastienでデバイスIDとして発行すれば試すことができます。詳しくは6および6に載せている資料をご覧ください。
https://github.com/gyokuro33/ai-agent

何を作ったか

こんなのです。

スマートフォン版

VR版

使用したもの

NTTドコモのAgentcraft : AIエージェント作成ツール
NTTドコモのSEBASTIEN : AIエージェント基盤
SEBASTIENと音声対話するためのUnity向けSDK
Oculus社のOculus RiftS もしくはOculus LinkでOculus Riftsと同様に操作可能にしたOculus Quest
Oculus社のOVRLipsync : 3Dモデルに音声に合わせた口パクを行わせるツール
Unity Technolosies Japanのunity chan! © UTJ/UCL : 3Dモデル。可愛いくて様々なポーズや表情が揃っている。違うキャラが作りたい人は自分の好きなキャラの3Dモデル
Unity(2018.4.13f1) : ゲーム開発エンジン
適当なAndroidのスマートフォン

参考にした記事

ドコモAIエージェントAPI
【UnityC#講座】ユニティちゃんの表情を替える【SetLayerWeight】
キャラクターを喋らせる!OVR Lip Syncの導入から問題発生時の解決法まで
Unityで作る!ドコモAIエージェントAPIを使った音声対話アプリ作成ハンズオン

構成図

アプリの仕組みは以下のようになっています。
基本アプリ側で行うのはグラフィックの描画と音声のInput,Outputのみで、それ以外の音声認識や音声合成、会話作成などは連携するAPI先で処理しています。

image.png

作り方

以下の手順に従っていけば、AIキャラクターを表示させておしゃべりを楽しむことができるようになります。

1. 使用したものの欄にあるものをすべてダウンロード

バージョン指定されているものはそのバーションのものを。バーション指定されていないものは最新のものをダウンロード。

2. Unityに空のプロジェクトを作成する

3. ダウンロードした以下の三つのファイルを作成したプロジェクトにインポートする

  1. SEBASTIENと音声対話するためのUnity向けSDK
  2. OVRLipsync
  3. Unity Chan

インポートの方法は以下の記事を参考にしてください。
パッケージのインポート

4. Asset/Sebastien/Scenes/mainのシーンを開く

image.png

5. 以下の記事を参考に、Unitychanが音声を再生することで口パクしてくれるようにする

キャラクターを喋らせる!OVR Lip Syncの導入から問題発生時の解決法まで
ただし最新のOVRLipsyncを使う場合ならVisemeToBlendTargetsの設定は記事とは異なり、以下のように設定する必要があります。
またUnity chan以外をしゃべらせたい場合は違う3Dキャラクターを同様の手順で設定して、口パクできるようにしてください。
image.png

6. 以下のハンズオン資料に従って、エージェントとの対話までできるようにする

Unityで作る!ドコモAIエージェントAPIを使った音声対話アプリ作成ハンズオン
対話内容はお好みで。この段階で、キャラがしゃべる声や会話内容などを作成します。
Agentcraftの詳しい使い方や作り方はここに載っています。
簡単に説明すると、会話については以下のように発話例とそれへの応答を決めることで、会話への対応パターンを作ることができます。
またキャラクターの声は以下のように、50以上の声から選ぶことができます。声のサンプルはここで聞くことができます。
さらにこれらの声は声質や話速、抑揚、声の高さなど様々なパラメーターをチューニングすることで、自分だけの声を作ることも可能です。
また以下の動画のように、会話中に声を変更することも可能です。
image.png
image.png

7. SpeakToLipsyncというC#スクリプトを書き、手順5で作成したLipSyncSoundに追加する

コードの内容は以下のもの。
書いている内容としては、Sebastienが合成した音声をOVRLipSyncで再生させることで、言った内容と同じことを口パクしてくれるようにする、というもの。

SpeakToLipsync.css
using System;
using UnityEngine;
using System.Diagnostics;
using Debug = UnityEngine.Debug;
using System.Threading;

public class SpeakToLipsync : MonoBehaviour

{
    public AudioSource currentAudioSource = null;
    public AudioSource speakAudioSource = null;
    public GameObject speakOb;

    private AudioClip beforeAudioClip;

    void Awake()
    {
        speakAudioSource = speakOb.GetComponent<AudioSource>();
        if (!currentAudioSource) currentAudioSource = GetComponent<AudioSource>();
        if (!currentAudioSource) return; 

    }
    // Start is called before the first frame update
    void Start()
    {
        currentAudioSource.mute = false;
        beforeAudioClip = speakAudioSource.clip;

    }

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

        if (beforeAudioClip != speakAudioSource.clip)
        {
            currentAudioSource.clip = speakAudioSource.clip;
            currentAudioSource.Play();
        }
        beforeAudioClip = speakAudioSource.clip;


    }

}

8. SpeakToLipsync.cssのSpeakObにHierarchyにあるSpeakを入れる

image.png

9. HierarchyにあるCanvasのGameObjectを非アクティブにする

10. Asset/Sebastien/Example内にあるExample.cssを以下のように変更する

関数Startの一番最後、speak.start();の次に以下の二行を入れてください。

speak.cancelPlay();//音声再生をキャンセルする
speak.unmute();

また関数onPlayEndに以下の一行を入れてください。

speak.unmute();

こうすることで、AIキャラクターが常に音声を聞いて反応してくれるようになります。

11. (VR版を試したい場合は)Player SettingのXR SettingからVirtual Reality Supportedにチェックを入れて、VRに対応できるようにする。

12. (android版を試したい場合は)使用プラットフォームをAndroidに変える

13. 各デバイスをPCにつなぐ

14. アプリをBuild&Runする

応用例

以上に書いた作り方は基本です。
これを基にさらに工夫を加えることで、より人間のような応答をAIキャラクターがしてくれるようになります。

1. AIキャラクターの応答に表情を加える

DocomoのAgentcraftには言葉以外のメタ情報を応答の際に送るという機能が存在します。
その機能を使って、AIキャラクターが応答する際に以下のような感情の情報も一緒に送り込み、その感情に応じた表情をさせることで、AIキャラクターが表情豊かに話してくれます。
表情の変え方は以下の記事を参考にすれば簡単に変えることができます。
詳しい作り方は、いいねが50超えたら書きます……(力尽きた感)
【UnityC#講座】ユニティちゃんの表情を替える【SetLayerWeight】
image.png

2. AIキャラクターが会話を記憶して、その情報を基に話すようにする

DocomoのAgentcraftには会話中で発された言葉を記憶して、会話を作成する機能があります。
例えば「私のことを覚えていますか?」 という質問に対して、その人と話したことがあるかで、以下のように分岐を作ることができます。
image.png

この機能を活用すると、以下のように、自分の好みなどのパーソナライズな情報を汲んでくれる、まさしく俺の嫁を作ることができます!

image.png

3. AIキャラクターを攻略できるようにする

応用2のさらなる応用です。
このAIキャラクターには情報を記憶させることが可能です。
そして会話にはその記憶から分岐を作ることが可能です。
そしてこの記憶は会話内以外にもあらかじめ設定することが可能です。
つまり、AIキャラクターと会話して、AIキャラクターの好みに合った返答を行えば好感度が上昇or下降するようにする。そうした会話を多数作れば、会話するにしたがってどんどん好感度が上昇(もしくは下降)していく、攻略可能な俺の嫁が誕生します!
ただ一つ問題点があり、自分で作るとそこらへんの好みや会話をすべて知っている状態で会話をすることになり、非常に虚しいことです……
この記事を読んだ誰かに作ってほしい……

まとめ

以上のように作っていくことで、自分の好きな声・キャラでおしゃべりしてくれるVRアシスタントを作ることができるようになります。
ぜひみんなで自分だけのキャラクターを作って、楽しんでください。
またこのアプリはSEBASTIENのデバイスIDを使用することで作成したキャラクターのAIの振る舞いを試すことができます。
つまり、他の人が育成したキャラクターとおしゃべりすることもできるんです!
この記事を読んでくれた方は、ぜひ自分だけのキャラクターを作り、それを公開することで色んなキャラクターを生み出してほしいです!

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

C#でunsafeを使わないポインタ

C# その2 Advent Calendar 2019、19日目の記事です!

本記事は、どこにも使えなさそうな、でもちょっと面白いなぁみたいなのが好きな人向けに書かれています。
もしかしたらこのアドベントカレンダーに適していない話題かもしれませんが、よかったら楽しんでください。

そもそもunsafeってなんぞ?

C#にはunsafeという黒魔術があります。
unsafeというのはC#の機能の一つで、その中ではC/C++やその他低レベルプログラミング言語のようにポインタを使うことができます。
もしあなたがポインタを安全に使いこなすことができるなら、unsafeは黒魔術でもなんでもない強力な武器になりますが、C#は初心者でも安全に書けることを目標にしている部分があるので基本的には使われません。

具体的には次のように使います。

unsafe {
  int x = 10;
  int* p = &x;
}

この、int*という型がポインタ型を表していて、pの中にはxのアドレスが入ります。そして、

  *p = 20; //x -> 20

とすることで、pの中のアドレス、つまりxを書き換えることができます。

簡単にポインタの役割をまとめると、

  • xのメモリ上のアドレスを保管しておく
  • xを直接見なくてもxを変更することができる

すごく簡略化しますが、こんな感じになります(二つ目の性質は大切なので覚えておいてください)

このままではunsafeの説明だけで一つの記事が出来上がってしまうので、ここらへんで終わりにします。もっと知りたい方は、このサイトの解説がとても分かりやすいです。

何を作ったのか

結論からいうと、今説明したunsafeを使わずにポインタ(っぽい何か)を作りました。
具体的には、さっき出てきた「xを直接見なくてもxを変更することができる」という性質だけに注目して、メモリ上の場所はわからんけど読み書きできるしポインタっぽいな、みたいなシステムを構築しました。

例えば次のようなコードがあったとします。

delegate void dele(int x); //整数を取って何も返さないデリゲート
class cls {
    private int value = 10; //なんかの情報
    public dele get_ptr() => (int v) => value = v; //値を変更する関数を返す
    public void print() => Console.WriteLine($"value = {value}.");
}


class Program {
    static void Main() {
        cls c = new cls();
        c.print(); //value = 10.

        dele func = c.get_ptr();
        func(20);

        c.print(); //value = 20.
    }
}

delegateについても一つの記事ができてしまうので深くは触れませんが、関数の型と認識すればここでは大丈夫です。これについてもこのサイトにわかりやすい解説があります

このコードでは、xがprivateであるにも関わらず10から20に変更することができました。
メゾットが用意されているのでそれはそうなのですが、重要なのはfuncという関数を通して間接的にアクセスしたという部分です。
ここで、funcをdele型の変数として保存しておき、好きな時にfuncを実行できたらどうでしょうか。また、funcをコピーして、たくさんのオブジェクトに配ったらどうでしょうか。
それらはfuncを通じてxに読み書きできるようになります!これってポインタじゃないですか!?(たぶん違う)

この考えを発展させ、SafePointerとSafeEntityの二つのクラスを書いてみました。
SafeEntityではT型の値と、そこからポインタ型のSafePointerを生成するメゾットのセットになっていて、SafePointerではnullチェックなどを行っています。
さっきのfuncに当たるのは、SafePointerのインスタンスとなります。

class SafePointer<T> {
    public delegate T Deref(); //Dereference つまり値の取得
    public delegate void Indir(T a); //Indirection つまり値の書き込み
    public bool isnull { get; private set; } = true; //値がnullかどうか

    private Deref _deref;
    private Indir _indir;

    public Deref deref {
        get {
            nullcheck();
            return _deref;
        }
        private set { _deref = value; }
    }
    public Indir indir {
        get {
            nullcheck();
            return _indir;
        }
        private set { _indir = value; }
    }

    public SafePointer(Deref d, Indir i) {
        deref = d;
        indir = i;

        isnull = false;
    }
    public SafePointer() { //値がまたないけどのポインタを作りたいときのコンストラクタ
        isnull = true;
    }

    private void nullcheck() {
        if (isnull) throw new NullReferenceException("値が空です");
    }
}

class SafeEntity<T> { //ポインタを返せる実体
    private T value;

    public SafeEntity(T x) {
        this.value = x;
    }

    public SafePointer<T> getrf() => //ポインタの取得(GET ReFerence)。C/C++でいう "&" 演算子
        new SafePointer<T>(
            () => this.value, //読みだし
            (T a) => this.value = a //書き込み
        );

    public void print() {
        Console.WriteLine($"value = {value}.");
    }
}

これを使って、次のCのコードを書き直してみます。

int main() {
  int x = 123;
  int *p = &x;

  printf("%d",*p); //123
  *p = 456;
  printf("%d",*p); //456

  return 0;
}

これをこうして...こうじゃ!
( ^ω^)
≡⊃⊂≡

static void Main(string[] args) {
    SafeEntity<int> x = new SafeEntity<int>(123);
    SafePointer<int> p = x.getrf(); //ポインタ取得

    Console.WriteLine(p.deref()); //123
    p.indir(456);
    Console.WriteLine(p.deref()); //456
}

C#でもポインタが使えましたね(にっこり)

ちなみに、C言語で言うダングリングポインタを作ろうと、次のようなコードを動かすと、

SafePointer<int> p;
{
    SafeEntity<int> e = new SafeEntity<int>(123);
    p = e.getrf();
}
p.indir(200);
Console.WriteLine(p.deref());

このコードはしっかり動作して、200と表示されます。
これは、classつまり参照型でスタックではなくヒープに実体が作られるからなのですが、例によって解説はしません。詳しく知りたい方は、ここにわかりやすく纏まっています。

実際に何か作ってみる

さて、ここまで頑張ってポインタを作ったので、何かその恩恵を享受したいところですね。
ということで、ここでは例としてリンクリストを作ってみます。機能としては最低限、

  • 要素の追加
  • 値の削除
  • 要素の表示
  • 要素のアクセス(indexerを使ってそれっぽく)

くらいがあればまぁいいでしょう。手抜き感が否めませんが、とりあえず実装してみました。

class LinkedListIterator<T> {
    public SafePointer<LinkedListIterator<T>> next; //次を指すポインタ
    T value;

    public LinkedListIterator(T x) {
        next = new SafePointer<LinkedListIterator<T>>();
        value = x;
    }
    public LinkedListIterator() {
        next = new SafePointer<LinkedListIterator<T>>();
        //valueは初期化されてない
    }

    public void Add(T x) {
        if (next.isnull) next = new SafeEntity<LinkedListIterator<T>>(new LinkedListIterator<T>(x)).getrf();
        else next.deref().Add(x); //再帰的に
    }

    public void RemoveAt(int i) {
        if(i == 1) next = next.deref().next; //消す直前まで来たら
        if (next.isnull) throw new IndexOutOfRangeException($"値が範囲外です {i}"); //次の値がなかったら
        else next.deref().RemoveAt(i - 1);
    }

    public void printForward() { //自身から前に向かって表示していく
        Console.Write(value);
        if (!next.isnull) { 
            Console.Write(",");
            next.deref().printForward();
            return;
        }
        Console.WriteLine();
    }

    public T this[int i] {
        get {
            if(i == 0) return value; //目的のインデックスに到着
            if (next.isnull) throw new IndexOutOfRangeException($"値が範囲外です。{i}");
            else return next.deref()[i - 1];
        }
        set {
            if (i == 0) this.value = value;
            if (next.isnull) throw new IndexOutOfRangeException($"値が範囲外です。{i}");
            else next.deref()[i - 1] = value;
        }
    }
}

class LinkedList<T> {
    LinkedListIterator<T> head; //0番目の要素を追加してプログラムを簡略化

    public LinkedList() {
        head = new LinkedListIterator<T>();
    }

    public void Add(T x) {
        head.Add(x);
    }
    public void printAll() {
        head.next.deref().printForward();
    }

    public T this[int i] {
        get { return head[i + 1]; } //ヘッダの一つ分ずらして実行
        set { head[i + 1] = value; }
    }

    public void RemoveAt(int i) {
        head.RemoveAt(i + 1);
    }
}

簡単に解説すると、
Add関数ではnextのisnullがfalseになる最後尾まで進み、新しく要素を作ってそのポインタを突っ込んでいます。もしCで実装するならmallocを使って新しく領域を確保しますが、ここでは参照型として自動でヒープに確保されることを利用し、少し気持ちの悪いコードで実現してみました。(条件次第ではガベコレに回収されそう)
RemoveAtやindexerなども同じで、範囲外の場合はちゃんとExceptionを投げるように設計しています。

ちなみに、すべて再帰を使ってるので、要素数が9930を超えたあたりからスタックオーバーフローで落ちます


(^^)


また、単純な再帰ではないのでとても遅くなります。(と思ってましたが解決方がありました。追記にあります)
検証コード

Stopwatch sw= new Stopwatch();
int Sum = 0;

sw.Start();

var list = new List<int>();
for(int i = 0;i < 5000;i++) list.Add(i);
for(int i = 0;i < 5000;i++) Sum += list[i];

sw.Stop();

Console.WriteLine("Normal list:" + sw.ElapsedTicks);

sw.Reset();
sw.Start();

Sum = 0;
var linkedList = new LinkedList<int>();

for (int i = 0;i < 5000;i++) linkedList.Add(i);
for(int i = 0;i < 5000;i++) Sum += linkedList[i];

sw.Stop();

Console.WriteLine("My list:" + sw.ElapsedTicks);

結果
Normal list:1048
My list:21671170

はい。実に20679倍ですね。

さすがにこれでは締まらないので、すごく適当ですが書き直します。

class ImprovedLinkedList<T> {
    LinkedListIterator<T> head;

    public ImprovedLinkedList() {
        head = new LinkedListIterator<T>();
    }

    public void Add(T x) {
        LinkedListIterator<T> prev = head;
        while (!prev.next.isnull)
            prev = prev.next.deref();
        prev.Add(x);
    }
    public T this[int i] {
        get {
            SafePointer<LinkedListIterator<T>> p = head.next;
            for(int t = 0;t < i;t++)
                p = p.deref().next;
            return p.deref().value;
        }
    }
}

Normal list:1143
My list:21417001
Improved list:11419730

それでも9991倍ですが、スタックオーバーフローも回避できたし実用性は求めてないのでいいでしょう。

まとめ

今回わかったこと。

  • 普通じゃない方法でプログラミングするのは楽しい(但し実用性は考えないものとする)

もし業務でC#を使うならすでに用意されているものを使いましょう!
じゃないと、うっかり動作時間が20000倍になったり、たった5000個の要素でスタックーバーフローを起こしたり、リストに入れた値が、いつの間にかガベージコレクションにお片付けされていたなんてことが起こります。

ただ、個人的にこういう技術がとても好きなので、もっと広まってほしいとも思います。

最後になりますが、ここまで見ていただきありがとうございました。
誤字脱字・技術的な間違い・改善案など、もしありましたらコメントかTwitterまで連絡をしていただけるとありがたいです。

こんな記事が皆さんの明日の話題になれば幸いです。

追記

再帰が処理のボトルネックになる理由として、

  • スタックを使いつぶしている
  • ループに比べて再帰が遅い

というのがありました。ところで、再帰を高速化する方法の中に「末尾呼出しの最適化(tail-call optimization)」というのがあります。
これは、関数内で自身を呼び足す際、「最後に呼び出してるんだったら自分の領域はもう開放しちゃってもいいよね?」といった感じでスタックを使いつぶさずに済む技術です。
今までC#では実装されていないと思い込んでいたのですが、x64では有効になるらしく、Release,x64で実行した結果、

Normal list:1155
My list:3423799

たったの2964倍遅いだけです!速っ!

本当にスタックのおかげで高速化されてるか疑問の方もいるかと思いますが、再帰する部分に

Console.WriteLine(new StackTrace().FrameCount);

と書いて実行すればスタックをたくさん使ってないことがわかると思います。

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