vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

ASP.NET Core でのログ出力( ILogger + NLog )

はじめに

今回はログ出力を追ってみることにします。

.NET でログを出力するライブラリはたくさんあるようですが、今回は組み込みである ILogger を使ってみます。

内容としては ILogger の使い方、特にファイルに内容を出力する方法が中心になる予定です。

httpsについて

いきなり違う話から入るのですが、 ASP.NET Core2.1 でプロジェクトを作ると https://localhost:5XXXhttps でページを開くようになります。
(Visual Studio でプロジェクトを作る場合、プロジェクト生成時にチェックを外すと http で開くよう設定を変更できます)

それは良いのですが、 Firefox では証明書が不正となりページが開けません。

ググってみたところ、 https://localhost:(ポート番号) を例外として設定する以外に開く方法はなさそうです。

localhost なんだし多めに見て、という気持ちはありますが、まぁ仕方ありませんね。

実際のリリース時には正しい証明書などを用意することとして、開発中は例外設定してしまいたいと思います。

ログ出力してみる

気を取り直して、とりあえずログを出力してみます。

例えば HomeController.cs からログを出力したい場合、 HomeController クラスのコンストラクタに ILogger< HomeController> を渡します。

HomeController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace WebApplication1.Controllers {
    public class HomeController : Controller {
        private readonly ILogger< HomeController> logger;

        // DI でインスタンスをセット.
        public HomeController(ILogger< HomeController> logger) {
            this.logger = logger;
        }
        public string Index() {
            // INFO レベルでログを出力.
            logger.Log(LogLevel.Information, "Hello work!");

            return "hello";
        }
        ~省略~
    }
}
  • ILogger< TCategoryName> の型は、ログの「カテゴリ」を指定するためのもので、
    文字列で指定することもできるが、ログを書き込むクラスを指定する必要があるため型を渡す方が簡単にできます。
  • 「カテゴリ」が何を指すのかは正確にはわからなかったのですが、そのログがどのクラスから出力されたのかを判別するもの、
    というくらいのものであるようです(そのためクラスの完全修飾名が必要)。

Service への追加?

DI ということで、以前と同じように Startup クラスで Service に追加…

する必要はありません。

Program クラスで下記を使用している場合、デフォルトのログ出力設定が CreateDefaultBuilder の中で行われます。

Program.cs

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace WebApplication1 {
    public class Program {
        public static void Main(string[] args) {
            CreateWebHostBuilder(args).Build().Run();
        }
        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup< Startup>();
    }
}

WebHost.cs

// Decompiled with JetBrains decompiler
// Type: Microsoft.AspNetCore.WebHost
~省略~
public static IWebHostBuilder CreateDefaultBuilder(string[] args) {
    IWebHostBuilder hostBuilder = new WebHostBuilder()
        ~省略~
      .ConfigureLogging((Action) ((hostingContext, logging) =>
      {
        logging.AddConfiguration((IConfiguration) hostingContext.Configuration.GetSection("Logging"));
        logging.AddConsole();
        logging.AddDebug();
      }))
    ~省略~
}

ILoggerFactory

なおググっている中で、 Startup の Configure の引数として ILoggerFactory を渡しているものもありました。

Startup.cs

~省略~
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
  ~省略~  
}

で、この ILoggerFactory でも AddConsole や AddDebug を実行することはできるようでした。

両者の使い分けに悩むところですが、少なくとも Microsoft Doscs を見る限りだと前者を使う方が良さそうです。
(後者はダメとまでは書かれていませんが、特に触れられていないため)

docs.microsoft.com

NLogを試してみる

Console に出力する分には問題がないのですが、ファイル出力したい今回のような場合、 Provider を自分で作る(またはサードパーティーのものを使う)必要があるようです。

今回は NLog を使ってみることにしました。

NLog の Wiki を基に進めていきます。

github.com

インストール

NuGet で NLog.Web.AspNetCore をインストールします
(いくつか種類がありますが、 ASP.NET Core 用のものを選びます。 ver.4.6 でした)。

ファイルを追加する

1.NLog の設定ファイル( nlog.config )をプロジェクト直下に作ります。 2.nlog.config のプロパティで、 詳細設定 > 出力ディレクトリにコピー の値を「常にコピーする」に変更します。

f:id:mslGt:20180906043538j:plain

※画像は Visual Studio ですが、 Rider の場合はファイル上で右クリック > Properties > Copy to output directory から設定できます。

3.Program.cs で NLog を有効にします。

Program.cs

using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using NLog.Web;

namespace WebApplication1 {
    public class Program {
        public static void Main(string[] args) {
            // nlog.configの読み込み.
            var logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
            try {
                CreateWebHostBuilder(args).Build().Run();
            }
            catch (Exception ex) {
                logger.Error(ex, "Stopped program because of exception");
                throw;
            }
            finally {
                NLog.LogManager.Shutdown();
            }
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup< Startup>()
                .ConfigureLogging((hostingContext, logging) => {
                    // NLog 以外で設定された Provider の無効化.
                    logging.ClearProviders();
                    // 最小ログレベルの設定.
                    logging.SetMinimumLevel(LogLevel.Trace);
                })
                // NLog を有効にする.
                .UseNLog();
        }
    }
}

nlog.configを読む

Wiki の「2.Create a nlog.config file.」を見てみると、結構複雑な感じがしてきます。

とはいえ個別にコメントを書いてくれているのと、 Wiki に nlog.config の個別ページも用意されているため、追いかけるのは容易そうです。

github.com

  • nlog: nlog.config 全体の設定、 internal-nlog.txt の出力設定を行います。
  • extensions: nlog.config の中で使われる、「${ }」のような表現方法を有効にするための機能を有効にします。
  • targets: ログの出力タイプ(ファイルとかコンソールとか)、名前( rules でこの値を使ってターゲットを指定します)、ファイルの場合はその出力先などを指定します。
  • rules: targets の中で設定したそれぞれのターゲットに、どのレベル( Info, Warning など)のログを出力するかなどのルールを指定します。

出力されるファイルは下記の三つです。

  • internal-nlog.txt: NLog の内部処理に関するログが出力されます。
  • nlog-all-${shortdate}.log: Trace レベルのログを出力します。
  • nlog-own-${shortdate}.log: Microsoft.* 名前空間下の Info レベルのログを除く、Trace レベルのログを出力します。

ログレベルについては下記参照、ということなのですが、Trace レベルが「メソッド X の始め、メソッド X の終わり、etc.」のように書かれていて、あまりよく分かりませんでした(- _ -;)

github.com

出力されたものを見ると、呼ばれたメソッドの情報が出力される、ということのようでした。

また rules で設定されるルールで、 nlog-all-${shortdate}.log と nlog-own-${shortdate}.log のルールは同じように見えるのですが、間に final="true" を持ったルールが追加されることで、Microsoft.* 名前空間下の Info レベルのログが除かれる結果となっています。

nlog.config

~省略~
  < rules>
    < !--All logs, including from Microsoft-->
    < logger name="*" minlevel="Trace" writeTo="allfile" />

    < !--Skip non-critical Microsoft logs and so log only own logs-->
    < logger name="Microsoft.*" maxLevel="Info" final="true" /> < !-- BlackHole without writeTo -->
    < logger name="*" minlevel="Trace" writeTo="ownFile-web" />
  < /rules>
  ~省略~

nlog.configを書く

ではこのサンプルを基に、自分でも nlog.config を書いてみようと思います。

nlog.config

< ?xml version="1.0" encoding="utf-8" ?>
< !-- NLog 内部のログはいったん出力しないこととする -->
< nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true">

  < extensions>
    < add assembly="NLog.Web.AspNetCore"/>
  < /extensions>

  < targets>
    < !-- コンソールに出力  -->
    < target xsi:type="Console" name="alloutput"
            layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />

    < target xsi:type="File" name="ownFile-web" fileName="C:\temp\logs\${date:format=yyyy}\${date:format=MM}\nlog-own-${shortdate}.log"
            layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />

    < target xsi:type="File" name="ownFile-access" fileName="C:\temp\accessLogs\${date:format=yyyy}\${date:format=MM}\nlog-access-${shortdate}.log"
            layout="${longdate},${message} ${exception:format=tostring}" />
  < /targets>
  < rules>
    < !--コンソールには Trace レベル以上のログすべてを出力-->
    < logger name="*" minlevel="Trace" writeTo="alloutput" />
    < !--Microsoft.* のクラスの Info レベル以下のログはスキップ-->
    < logger name="Microsoft.*" maxLevel="Info" final="true" />
    < logger name="*" minlevel="Info" writeTo="ownFile-web" />
    < !-- HomeController に対するアクセスログ( Info レベル)のみ記録 -->
    < logger name="WebApplication1.Controllers.HomeController" minLevel="Info" maxLevel="Info" writeTo="ownFile-access" />
  < /rules>
< /nlog>

これで例えば今日( 2018.09.06 )実行した場合、下記の場所にファイルが出力されます。

  • C:\temp\accessLogs\2018\09: HomeController にアクセスしたときのログ
  • C:\temp\logs\2018\09: Microsoft.* 以下のクラスを除くアプリケーション全体での、Info レベル以上のログ

※ NLog 内部処理のログを出力しない設定にしていますが、本来出すべきだとは思います。

おわりに

一応これでログ出力はできるようになりましたが、まだいくつか課題はあります。

  1. ログがどのように出力されるかわかっていない → Program.cs に追加した NLog 関連のコードからたどっていくのが良さそう?
  2. ログを出力するレベルや、フォーマットなどログ設計について学習が必要
  3. Visual Studio で nlog.config を編集しているときに、設定が正しく反映されないことがあった
  4. Rider で nlog.config を開くと xsi:type="Console" などでエラーとして表示(ビルドや実行には問題なし)

3.については Program.cs でログ出力すると直ったり、4.は nlog.config に限らず IntelliJAndroid Studio などでも見かけた現象だったりするので、改善の余地もありそうです。

これらについてはおいおい解決していきたいと思います。

また今回は触れませんでしたが、 NLog の出力先として DB やメールなども利用可能(らしい)ですので、そちらも試してみたいと思います。

参照

https

ILogger

NLog

Rider2018.2で日本語コメントがTypo扱いされる

はじめに

Rider2018.2 にアップデートしたところ、日本語がほぼ全て Typo 扱いされるようになりました。

f:id:mslGt:20180830071802j:plain

この辺りの話を見ていると、スペルチェックの機能が変更されたのが原因のようです。

もんりぃ先生👨‍🏫マンガでわかるUnity連載中! on Twitter: "Rider 2018.2 EAP は日本語コメントが軒並み typo 扱いになる…。まぁ、「英語で書けやー!」って言われたら「あ、はい。ごめんなさい。」って感じだけどw"

blog.jetbrains.com

今後の更新で英語以外でも正しくスペルチェックが効くようになるんじゃないかな?と期待はしているのですが、現状の対処法としては

  1. Typo 扱いされても気にしない
  2. コメントを片っ端から辞書登録
  3. コメントに対するスペルチェックを無効にする

があると思っています。

1.が最強ではありますが、やっぱり気になる。2.は現実的ではないし。。。ということで、3.をやってみたのでそのメモです。

設定を変更する

スペルチェックの項目というと、Setting > Editor > Spelling > ReSpeller が引っかかるのですが、今回の目的には合致しないようでした。

コメントに対するスペルチェックを無効にするには、 Settings > Inspection Settings > Inspection Severity > C# > Spelling Issues > Typo in comment のチェックを外します。

f:id:mslGt:20180830073040j:plain

あとは Save ボタンを押せば OK です。

せっかくの機能を止めてしまう、というのは心苦しくはあるので、もう少し良い解決方法があればとは思うのですが。

参照

ASP.NET Coreのプロジェクトを発行(Publish)してみる

はじめに

これまで ASP.NET Core を実行するとき、Visual Studio や Rider 上で実行していました。

が、実際にはビルドしてアプリケーション単体で実行する必要があるため、これを試してみることにしました。

今回は組み込み Web サーバーである Kestrel だけを使う方法を試します。

Webサーバーについて

まず勘違いをしていたのですが、何となく ASP.NET Core で作った Web アプリケーションは、IIS などの Web サーバー上でないと動かないものだと思っていました。

実際のところは ASP.NET Core > Kestrel という構成で元々動作するものの、 Kestrel では機能が不足しているために ASP.NET Core > Kestrel > IIS などの Web サーバー(リバースプロキシ)という構成をとるものが多い、ということのようです。

組み込みの Web サーバーを使うのは SpringBoot でも同じですね。

バースプロキシを使う必要性については、下記が分かりやすいと感じました。

docs.microsoft.com

なお Kestrel を使う他にも方法があるようですが、ここでは Kestrel を使用することとします。

IIS と組み合わせる方法については次回以降トライする予定です。

話は戻って、この辺りの区別がついていなかったために後述のプロファイル作成などで結構迷ってしまうことになりました。

プロジェクトを発行する

では張り切ってプロジェクトを発行してみましょう。

Visual Studio の場合、 ビルド > WebApplication1 (プロジェクト名)の発行 から行うことができます。

プロファイルの作成

初めに発行のためのプロファイルを作成します。

f:id:mslGt:20180821214623j:plain

(最初はこの画面じゃなかった気がするのですが) この画面が表示されたら、 新しいプロファイル > フォルダー > 発行 で、 ASP.NET Core > Kestrel の構成で動作させるためのプロファイルが作成できます。

元の画面に戻ったら、 発行 ボタンを押せば(デフォルトでは bin\Release\netcoreapp2.0\publish に)実行のためのファイルが出力されます。

これに成功すれば、あとはターミナルなどで bin\Release\netcoreapp2.0\publish に移動し、下記のコマンドで実行すれば OK です。

dotnet WebApplication1.dll

外部アクセスを許可する

これまですっかり忘れていたのですが、デフォルトの状態だと実行中の ASP.NET Core のサイトに、同一ネットワークに接続している他の端末からアクセスすることはできません。

理由は二つあります。

  1. http://localhost:5XXX」のような URL ではアクセスできても、「http://IPアドレス:5XXX」ではアクセスできない
  2. Sinatra などと同様、デフォルトでは外部からのアクセスを制限している

これを何とかしてみます。

1.URL を変更する

Kestrel 単体で動かす場合

IIS Express などを使わず Kestrel 単体で動かす場合、プロジェクトフォルダ > Properties にある launchSettings.json を変更すれば OK です。

launchSettings.json

{
  ~省略~
  "profiles": {
    ~省略~
    },
    "WebApplication1": {
      "commandName": "Project",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://変更したいIPアドレス:ポート番号/"
    }
  }
}

IIS Express を使う場合

IIS Express を使う場合も launchSettings.json を変更するのですが、それだけではエラーになってしまいました。

launchSettings.json

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://変更したいIPアドレス:ポート番号/",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    ~省略~
  }
}

ググってみると、 IIS Express 側の設定も変更する必要があり、 applicationhost.config を変更すればできる、とのこと。

ということで、ドキュメントフォルダ > IISExpress > config 以下にある applicationhost.config を変更したのですが、特に何も変わらないorz

さらにググってみると、どうやら別のところの applicationhost.config を見ているらしいと。

結局 ASP.NET Core ver.2.0 では プロジェクトフォルダ > .vs > config に置かれていました。

applicationhost.config

< site name="WebApplication1" id="2">
        < application path="/" applicationPool="WebApplication2 AppPool">
          < virtualDirectory path="/" physicalPath="プロジェクトフォルダまでのパス\WebApplication1\WebApplication1" />
        < /application>
        < bindings>
                    < binding protocol="http" bindingInformation="*:5XXX:変更したいIPアドレス" />
                    < binding protocol="http" bindingInformation="*:5XXX:localhost" />
        < /bindings>
      < /site>
  • 5XXX はポート番号です。念のため。

そしてやっぱりエラー

ええ、これでいけるぜやったぜ!と思いながら実行しました。

やっぱりエラーになるとorz

どうやら IIS Express で localhost 以外のアドレスを指定したい場合、Visual Studio を管理者として実行する必要があるようです。

とりあえずこれで localhost 以外のアドレスを扱えるようになりました。

あとは ACL(Access Control List) やファイヤーウォールの設定を行うことで外部からもアクセス可能になります。

IIS Express のWebサイトに他のホストからアクセスできるようにする - ASP.NETアプリケーションのデバッグ時に他のホストからのアクセスをする

2.外部からのアクセスを許可する

外部からのアクセスを許可するには、 Program.cs で、 Listen する URL を指定する必要がある、ということです。

Program.cs

using System.IO;
using Microsoft.AspNetCore.Hosting;

namespace WebApplication1 {
    public class Program {
        public static void Main(string[] args) {
            BuildWebHost(args).Run();
        }
        public static IWebHost BuildWebHost(string[] args) =>
            new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup()
                // すべてのアドレスを許可.
                .UseUrls("http://0.0.0.0:0")
                .Build();
    }
}

これで OK 。。。のはずだったのですが。

UseUrls が効かない

UseUrls を使っている・使っていないにかかわらず外部からアクセスできてしまいましたorz

ただ、例えば http://0.0.0.0:0http://1.2.3.4:5 のように変更するとエラーになったため、デフォルトでは制限なし、明示的に指定した場合のみ制限がかかる、ということかもしれません。

Empty でプロジェクトを作成したときに使用されている、 WebHost.CreateDefaultBuilder を見ても UseUrls は使用されていないようでした。

というわけで、結果としては 1.URL を変更する だけを設定すれば OK のようです。

なお今回は UseUrls を使う場合もハードコーディングしていましたが、JSON を使って環境によって切り替え、といったこともできるようです。

長くなってきたので今回は特にこれ以上触れません。

おわりに

localhost の代わりに IP アドレスを使うとかすぐできるでしょ。と高を括っていた割に時間がかかってしまいました(;゚Д゚)

次回は IIS を使ったリバースプロキシに挑戦してみる予定です。

多分。

※2018.08.24 追記

今回出力したファイルを使って、下記の方法で登録したら問題なく実行できました。

qiita.com

ということでやっぱり次回は Model についてかなぁ。

参照

IIS Express

IIS

【ASP.NET Core】Postで受け取った値をWebSocketで送信する

はじめに

今回はこの二つをつなげてみることにします。

流れとしては

  1. WebSocket で接続
  2. Post リクエストを送信する
  3. 2.で受け取った値を 1.に送信する

本当は WebSocket の送信、またはプッシュ通知を使うべきところのような気もしますが、とりあえず今回はこれでいくことにします。

WebSocketのコントローラーをインジェクトできるようにする

以前 WebSocket の通信処理は WebSocketController というクラスで行っており、それは Startup から呼び出していました。

今回はルーティングを行う Controller クラスからも( Post リクエストで値を送信する際)アクセスしたいので、 DI を使ってインジェクトできるようにします。

IWebsocketController.cs

using Microsoft.AspNetCore.Builder;
using WebApplication1.Domains;

namespace WebApplication1.Controllers {
    public interface IWebsocketController {
        void Map(IApplicationBuilder app);
        void SendMessage(Message message);
    }
}

この interface を WebSocketController (クラス)で継承します。

ここで注意が必要なのは、 DI でインジェクトするために WebSocketController のコンストラクタを public にする必要があり、インジェクトする interface も public にする必要がある、ということです。

で、以前試した方法と同じように Startup クラスからインジェクトします。

Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WebApplication1.Controllers;

namespace WebApplication1 {
    public class Startup {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration) {
            Configuration = configuration;
        }
        public void ConfigureServices(IServiceCollection services) {
            services.AddSingleton< IWebsocketController, WebsocketController>();
            services.AddMvc();
        }
        ~省略~
    }
}

そしてインジェクトされる Controller クラスです。

HomeController.cs

using System;
using Microsoft.AspNetCore.Mvc;
using WebApplication1.Domains;

namespace WebApplication1.Controllers {
    public class HomeController : Controller {
        private readonly IWebsocketController websocket;

        public HomeController(IWebsocketController setWebsocket) {
            websocket = setWebsocket;
        }
        
        ~省略~

        [Route("/PostSample")]
        [HttpPost]
        public void PostSample([FromForm]Message message) {
            // WebSocket で接続しているデバイスに Post リクエスト受け取った値を送信する.
            websocket.SendMessage(message);
        }      
    }
}

接続したWebSocketの情報をキャッシュする

前回トライしたとき、 WebSocket の接続、情報の送受信は下記のようにしていました。

WebsocketController.cs

using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace WebApplication1.Controllers {
    public class WebsocketController {
        public void Map(IApplicationBuilder app) {
            app.UseWebSockets();
            app.Use(async (context, next) => {
                if (context.WebSockets.IsWebSocketRequest) {
                    // HTTPリクエストの内容が WebSocket のものであればAcceptし、メッセージの受信・送信を行うメソッドに渡す.
                    WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
                    await Echo(context, webSocket);
                }
                else {
                    // HTTPリクエストの内容が WebSocket のものでなければエラー.
                    context.Response.StatusCode = 400;
                }
            });
        }

        // 引数のHttpContextは今回未使用. やりたいことがこれだけなら不要かも.
        private async Task Echo(HttpContext context, WebSocket webSocket) {

            // 送受信するデータ(buffer)のサイズ指定.
            byte[] buffer = new byte[1024 * 4];

            WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);

            // 接続がCloseされるまでメッセージの送受信を繰り返し.
            while (result.CloseStatus.HasValue == false) {
                await webSocket.SendAsync(new ArraySegment< byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

                result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);
            }
            // Whileを抜けたらClose処理.
            // 接続が Close されると、result.CloseStatusに「NormalClosure」のような値が入ってくる.
            await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
        }
    }
}

肝心の送受信のところは while でループしていて、 Post リクエストを受けたら送信、とするのは面倒そうです。

ということで、ここを変更してみます。

誰が WebSocket の通信に関する情報を持っているか

誰が WebSocket で通信しているクライアントの情報を持っているか、というと、 WebSocket クラスです。

そのままですね。

これを保持しておけば、ループ以外の場所からも送信ができるハズ。

一人だけ接続する場合

WebSocketController.cs

using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using WebApplication1.Domains;

namespace WebApplication1.Controllers {
    public class WebsocketController: IWebsocketController {
        private WebSocket webSocket;

        // DI によるインジェクト.
        public void Map(IApplicationBuilder app) {
            app.UseWebSockets();
            app.Use(async (context, next) => {
                if (context.WebSockets.IsWebSocketRequest) {
                    webSocket = await context.WebSockets.AcceptWebSocketAsync();
                    await Echo(context);
                }
                else {
                    context.Response.StatusCode = 400;
                }
            });
        }

        public async void SendMessage(Message message) {
            if (webSocket == null) {
                return;
            }
            byte[] additionalBytes = System.Text.Encoding.UTF8.GetBytes(message.BodyText);
            await webSocket.SendAsync(new ArraySegment< byte>(additionalBytes, 0, additionalBytes.Length),
                WebSocketMessageType.Text, true, CancellationToken.None);
        }
        private async Task Echo(HttpContext context) {
            byte[] buffer = new byte[1024 * 4];

            WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);

            while (result.CloseStatus.HasValue == false) {            
                result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);
            }
            await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
            webSocket.Dispose();
        }
    }
}

とりあえず一人だけ接続した体で書いてみました。

Echo で ReceiveAsync を残しているのは、接続が Close されたかを調べたいためです。

もちろん WebSocket で接続しているクライアント側からも送信したい場合は SendAsync も必要です。

ローカル変数がメンバー変数になっただけで、特に大きな違いはないかと思います。
送信時、 WebSocket で接続されていない、または接続を閉じた(+ Dispose) 場合に処理をスキップする辺りは多少注意がいるのかもしれません。

複数人で接続した場合

基本的には WebSocket をリスト化するだけです。

WebsocketController.cs

using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using WebApplication1.Domains;

namespace WebApplication1.Controllers {
    public class WebsocketController: IWebsocketController {
        private List webSockets = new List< WebSocket>();
        public void Map(IApplicationBuilder app) {
            app.UseWebSockets();
            app.Use(async (context, next) => {
                if (context.WebSockets.IsWebSocketRequest) {
                    WebSocket newWebSocket = await context.WebSockets.AcceptWebSocketAsync();
                    webSockets.Add(newWebSocket);
                    
                    await Echo(context, newWebSocket);
                }
                else {
                    context.Response.StatusCode = 400;
                }
            });
        }
        public async void SendMessage(Message message) {
            foreach (WebSocket w in webSockets) {
                if (w.State == WebSocketState.Closed) {
                    continue;
                }
                byte[] additionalBytes = System.Text.Encoding.UTF8.GetBytes(message.BodyText);
                await w.SendAsync(new ArraySegment< byte>(additionalBytes, 0, additionalBytes.Length),
                    WebSocketMessageType.Text, true, CancellationToken.None);
            }
        }
        private async Task Echo(HttpContext context, WebSocket webSocket) {
            byte[] buffer = new byte[1024 * 4];

            WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);
            
            while (result.CloseStatus.HasValue == false) {
                result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);
            }
            await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
            webSocket.Dispose();

            // 接続を切った後Listから削除するので、対象のインデックスはWebSocketを使って動的に取る.
            int index = webSockets.IndexOf(webSocket);
            if(index >= 0 && index < webSockets.Count) {
                webSockets.RemoveAt(index);
            }
        }
    }
}

コメントにも書きましたが、先ほど無くしていた Echo の引数の WebSocket ですが、接続が切れた WebSocket は List から外していくため RemoveAt を使うためには動的にインデックスを取る必要があります。

このインデックスを IndexOf で取得するためにもう一度 WebSocket を渡すようにしています。

以前 Spring boot で試したときは、 WebSocketSession が Id を持っていたためそれで判断していましたが、今回そのようなデータが取れないようだったため IndexOf を使っています。

で、この IndexOf 、どうやって WebSocket を比較しているのかを知りたかったのですが、これを書いている 2018.08.08 時点ではうまく情報を見つけることができませんでした。

これについては追加で情報が見つけられれば追記します。

MacでClosedXMLを使ってExcel開こうとしたらエラーになった話

はじめに

最近戯れに Teratail の C# に関する質問で、答えられそうなものを答えてみる、ということをしています。

今回は↓を解決したかったのですが、

teratail.com

何故か別の問題がでてしまい、それを解決したときのメモです。

環境

発生した問題

まず以下のようなコードを実行しようとしました。
(なお Windows 環境では問題なく動作することは確認しています)

Program.cs

class Program {
    private const string FilePath = @"HelloWorld.xlsx";
    private const string SheetName = "Sample Sheet";

    static void Main(string[] args) {
        using (var workbook = GetWorkbook()) {
            using (var worksheet = GetWorksheet(workbook)) {
                for (var i = 1; i <= 50; i++)
                {
                    // 空のセルが見つかるまでスキップし、値を入れたらそこで処理を終了.
                    if (worksheet.Cell("A" + i).Value.ToString() == "") {
                        worksheet.Cell("A" + i).Value = "Hello world";
                        break;
                    }
                }
            }
            workbook.Save();
        }
    }

    static XLWorkbook GetWorkbook() {
        if (File.Exists(FilePath)) {
            return new XLWorkbook(FilePath);
        }
        // ファイルが存在しなければ新規作成.
        var workbook = new XLWorkbook();
        // 1つ以上シートが存在する必要があるのでシートを追加.
        workbook.Worksheets.Add(SheetName);
        // ここでエラー. 
        workbook.SaveAs(FilePath);
        return workbook;
    }

    static IXLWorksheet GetWorksheet(XLWorkbook workbook) {
        if (workbook.TryGetWorksheet(SheetName, out var worksheet)) {
            return worksheet;
        }
        // シートが存在しなければ追加.
        return workbook.Worksheets.Add(SheetName);
    }
}

これを実行すると、 workbook.SaveAs(FilePath); の部分で TypeInitializationException が発生しました。

更に原因を追っていくと、どうやら libgdiplus というライブラリファイルが見つからないことが原因となっているようでした。

homebrewで起きた問題

libgdiplus は GitHub で開発が進められており、 README を基に、依存ファイルを homebrew でインストールし、 make install してみることにしました。

github.com

そして動かない homebrew orz

どうやら OS をアップデートした影響で、下記の問題が発生している様子。

qiita.com

ということで手順に従って権限を変更しようとしたのですが、エラーで失敗しましたorz

仕方がないので、 homebrew を再インストールすることにしました。

再インストール後、自動・手動で求められた依存ファイルのインストールを行い、なんとか homebrew が動作するようになりました。

解決

晴れて libgdiplus がインストールできるぞ! と思ったものの、 git clone した libgdiplus のディレクトリ上で make install を実行してもインストールするものはないよ。とエラーになってしまいました。

結果としては、 homebrew で libgdiplus を検索したときに見つけた、 mono-libgdiplus をインストールすることで解決しました。

いやぁ長かった。

おわりに

ともあれこれで Mac でも C#Excel 操作ができるようになったわけですね。

Enjoy Excel Life!

ってどんな締めや。

ASP.NET CoreでPOST (Unityもちょっとだけ)

はじめに

以前 Controller から受け取れるようにしたので、今回はそれに対する POST リクエストで、色々値を受け渡してみよう、という内容です。

文字列を受け取る

まずは Razor 構文の勉強がてら、文字列を Views/Home/Index.cshtml から渡してみます。

Index.cshtml

@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
< form method="POST" asp-controller="Home" asp-action="PostSample">
    < div>Name: < input name="bodyText"/>< /div>
    < input type="submit" />
< /form>
  • asp-controller でルーティングを行う Controller を指定できます。
  • asp-action で Submit ボタンを押したときに呼ばれるメソッドを指定できます。
  • TagHelpersのおかげで入力補完も効いて便利ですね。
  • input タグの name で、呼び出すメソッドの引数を指定します。

で、これを Controller で受け取ります。

HomeController.cs

~省略~
[Route("/PostSample")]
[HttpPost]
public void PostSample(string bodyText) {
    // 引数名は Input の name と合わせる必要がある.
    Console.WriteLine(bodyText);
}
~省略~

なお、引数を今回のようにフォームからの入力に限定したい場合は下記のように指定できます。

HomeController.cs

~省略~
[Route("/PostSample")]
[HttpPost]
public void PostSample([FromForm] string bodyText) {
    // 引数名は Input の name と合わせる必要がある.
    Console.WriteLine(bodyText);
}
~省略~

他にも、今回は POST ですが、例えば GET でクエリパラメーターを受け取りたい場合、 [FromQuery] を使います。

注意点を付け加えるとすると、メソッドのオーバーロードはできず、例えば引数なしのメソッドを追加すると、 Submit ボタン押下時に AmbiguousActionException が発生します。

配列を受け取る

次は配列を受け取ってみます。

Razor 構文では @{} で C# のコードが書けるため、下記のようなことができます。

Index.cshtml

@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
< form method="POST" asp-controller="Home" asp-action="PostSample">
    < div> Name:
        @{
            for (var i = 0; i < 3; i++) {
                
            }
        }
      < /div>
    < input type="submit" />
< /form>

HomeController.cs

~省略~
[Route("/PostSample")]
[HttpPost]
public void PostSample(string[] messages) {
    Console.WriteLine(messages[0]);
}
~省略~

キーとなるのは name で、これが同じものを配列(または List )として扱うことができます。

実際のユースケースを踏まえて ID を設定していますが、無くても動作には問題ありません。

HTML の左上の要素から順にセットされます。

自作クラスを受け取る

今度は自作クラスを引数として受け取ってみます。

Message.cs

using System;

namespace WebApplication1.Domains {
    public class Message {
        public int Id { get; set; }
        public string Title { get; set; }
        public string BodyText { get; set; }
        public DateTime LastUpdatedDate { get; set; }
    }
}

※今回は簡略化のため BodyText のみを使いますが、他の要素も同じように渡すことができます。

引数として受け取る

今回は値を受け取る方法を二種類試しました。

一つ目は先ほどと同じように、引数として受け取る方法です。

HomeController.cs

[Route("/PostSample")]
[HttpPost]
public void PostSample([FromForm] Message message) {
    Console.WriteLine(message.BodyText);    
}

Index.cshtml

@page
@using WebApplication1.Domains

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Message message = new Message();
}
< div>@DateTime.Now< /div>
< form method="POST" asp-controller="Home" asp-action="PostSample">
    < div>Name: < input name="message.BodyText" />< /div>
    < input type="submit" />
< /form>

余談ですが、上記の message.BodyText のようにして値を渡す方法は、 GET でも使用できます。

このような URL でクエリパラメータを渡すと、クラスとして受け取ることができます。

http://localhost:51817/PostSample?message.Id=1&message.BodyText=sample

HomeController.cs

~省略~
[Route("/PostSample")]
public void PostSample([FromQuery] Message message) {
    Console.WriteLine(message.BodyText);
}
~省略~

BindProperty を使う

引数として渡すのではなく、 BindProperty を使うこともできます。

HomeController.cs

~省略~
[BindProperty]
public Message Message { get; set; }

[Route("/PostSample")]
[HttpPost]
public void PostSample() {
    Console.WriteLine(Message.BodyText);    
}
~省略~

Index.cshtml

@page
@using WebApplication1.Domains

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
< div>@DateTime.Now< /div>
< form method="POST" asp-controller="Home" asp-action="PostSample">
    < div>Name: < input name="Message.BodyText" />< /div>
    < input type="submit" />
< /form>

値を他からも使いたい場合などは便利そうです。

asp-for

もう一つ、 Input に name ではなく asp-for を使って指定する方法( ID、Name が自動設定されるとのこと)がありますが、コードビハインドのある Razor View でないと、 viewData が null というエラーが発生したため今回は使用していません。

Unity からリクエストを投げてみる

HTML からだけでなく、 Unity から送ってみるとどうなるでしょうか。

GET リクエス

UnityWebRequest を使ってリクエストを投げることができます。

まずは GET から( ASP.NET Core 側は GET リクエストを受け付けるように変更してください)。

SendSample.cs

using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class SendSample : MonoBehaviour {
    public Button SendButton;

    public void Start() {
        SendButton.onClick.AddListener(() => {
            StartCoroutine(Send());
        });
    }
    private IEnumerator Send() {
        UnityWebRequest request = UnityWebRequest.Get(@"http://localhost:51817/PostSample?message.Id=1&message.BodyText=sample");
        yield return request.SendWebRequest();

        if(request.isNetworkError ||request.isHttpError) {
            Debug.Log(request.error);
        }
        else {
            Debug.Log("Form upload complete!");
        }
    }
}

POST リクエス

ASP.NET Core の View のように、クラスをそのまま渡すことはできないため、 JSON 形式にして渡すことにします。

SendSample.cs

using System.Collections;
using Domains;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class SendSample : MonoBehaviour {
    public Button SendButton;

    public void Start() {
        SendButton.onClick.AddListener(() => {
            var message = new Message {
                Id=6,
                BodyText = "sample",
            };
            var json = JsonUtility.ToJson(message);
            
            StartCoroutine(Send(json));
        });
    }
    private IEnumerator Send(string jsonFile) {
        WWWForm formData = new WWWForm();

        // そのまま渡すと{}がエンコードされてしまい、JSONとして認識されなくなるのでbyte[]に変換.
        byte[] postData = System.Text.Encoding.UTF8.GetBytes (jsonFile);
        
        UnityWebRequest request = UnityWebRequest.Post("http://localhost:51817/PostSample", formData);
        request.SetRequestHeader("Accept", "application/json");
        request.SetRequestHeader("Content-Type", "application/json");
        
        request.uploadHandler = new UploadHandlerRaw(postData);
        
        yield return request.SendWebRequest();
        
        if(request.isNetworkError || request.isHttpError) {
            Debug.Log(request.error);
        }
        else {
            Debug.Log("Form upload complete!");
        }
    }
}

ここで重要なのは、 JSON のデータを渡し忘れないことです(白目)

あとコメントにも書きましたが、JSON データを string でそのまま渡すとエンコードされてしまうそうなので、 byte[] で渡します。

JSON を受け取る

先ほど触れた通り、 Unity と ASP.NET Core でクラスを共有することはできないため、 JSON を受け取る必要が出てきます。

HomeController.cs

~省略~
[Route("/PostSample")]
[HttpPost]
public void PostSample([FromBody]Message message) {
    Console.WriteLine(message.BodyText);
}
~省略~

ポイントは [FromBody] で、これによって自動でデシリアライズされた値を引数として受け取ることができるようになります。

JSON を扱う場合は必ず [FromBody] をつける必要があり、これを外すと空データになってしまいます。

ただし、 Form データなどは受け取ることができない(不正なメディアタイプと怒られる)ため、注意が必要です。

おわりに

とりあえずデータは渡せるようになりました。

前にも書いた気がしますが、 WebSocket の処理と絡めてみたり、いい加減に Model を触ってみたりもしたいところ。。。

あとは Rx とか。

参照

Unity

【Unity】websocket-sharpのOnMessage

はじめに

前回作った WebSocket であれこれ試そうとしたら、 Unity で websocket-sharp を使って送信されたデータを受け取る、 OnMessage() でエラーが出たので調べた時のメモです。

なお Unity 側のコードはこれと同じです。

http://mslgt.hatenablog.com/entry/2017/05/11/073758

具体的には、OnMessage() が Main(UI) とは別の Thread から実行され、 Unity の処理(例: uGUI の Text に値を入れる、 Transform の値を変更する)を実行しようとするとエラーが発生していました。

WebSocketAccessor.cs

public class WebSocketAccessor : MonoBehaviour {

  public Text MessageText;
  private WebSocket ws;
    
    private void Start () {
    ws = new WebSocket("ws://localhost:5XXXXX/ws");
    
        // メッセージ受信時のイベント.
        ws.OnMessage += (sender, e) => {
          Debug.Log("Received " + e.Data);  


      // ここでエラー.
        MessageText.text = e.Data;

      
        };
        // 接続.
        ws.Connect ();
    }
    ~省略~

で、別 Thread から実行することが問題なのであれば、 Main Thread から実行するよう変更しましょう、というのが今回の内容です。

Main Threadで実行する

Java, Android では runOnUiThread というものがあります。

挙動は名前の通りなのですが、 C# ではこのようなものはなさそうです。

その代わりに、Main Thread で Context を取得しておき、それを通して Main Thread で実行する、ということが可能になります。

WebSocketAccessor.cs

public class WebSocketAccessor : MonoBehaviour {

  public Text MessageText;
  private WebSocket ws;
    
    private void Start () {
    ws = new WebSocket("ws://localhost:5XXXXX/ws");


    // Main ThreadのContextを取得する.
    var context = SynchronizationContext.Current;
    
    
        // メッセージ受信時のイベント.
        ws.OnMessage += (sender, e) => {
          Debug.Log("Received " + e.Data);  


      // Main Threadで実行する.
      context.Post(state => {
        MessageText.text = state.ToString();
      }, e.Data);

      
        };
        // 接続.
        ws.Connect ();
    }
    ~省略~
  • SynchronizationContext.Current を Main Thread で実行すると、 Main Thread の Context を取得することができます。

SynchronizationContext.Post()

SynchronizationContext.Post() から Main Thread での処理を呼び出すことができます。

この第一引数は SendOrPostCallback という delegate method で、第二引数は第一引数に渡す引数です。

この引数の型は Object なので、今回の string 以外のデータも渡すことができますが、 delegate method で型変換は必要です。

SynchronizationContext.Send()

もう一つ似たようなものに SynchronizationContext.Send() というものがあります。

パッと見同じに見えるのですが、どうやら Post() は非同期的に実行されるのに対し、 Send() は同期的に実行される、という違いがあるようです。

そのため、例えば下記の2つを比べると。。。

SynchronizationContext.Post()

// メッセージ受信時のイベント.
ws.OnMessage += (sender, e) => {
            
    Stopwatch watch = new Stopwatch();
    watch.Start();

    context.Post(state => {
    // 1000ミリ秒止める.
        Thread.Sleep(1000);
        MessageText.text = state.ToString();
    }, e.Data);
            
    Debug.Log("finish " + watch.ElapsedMilliseconds);
    watch.Stop();
};

SynchronizationContext.Send()

// メッセージ受信時のイベント.
ws.OnMessage += (sender, e) => {
            
    Stopwatch watch = new Stopwatch();
    watch.Start();

    context.Send(state => {
    // 1000ミリ秒止める.
        Thread.Sleep(1000);
        MessageText.text = state.ToString();
    }, e.Data);
            
    Debug.Log("finish " + watch.ElapsedMilliseconds);
    watch.Stop();
};

Post() は「finish 0」と出力されるのに対し、 Send() は「finish 1009」と出力されました。

なお、上記で Debug.Log("finish " + watch.ElapsedMilliseconds); が実行され、 Editor 上で表示されるタイミングは(おそらく MainThread で実行されるとかその辺の理由で)どちらも同じのため、注意が必要かもしません。

おわりに

以前 SpringBoot で動かしていた時には OnMessage も MainThread で受け取れていた気がしたのですが、今回変わった理由はまだよくわかっていません。

まぁ websocket-sharp 側で変更があったのかなぁ、というところではありますが。

次回も WebSocket(ASP.NET Core 側)で遊んでみる予定です。

参照