xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

キャリッジリターン (CR) を無視する ModelBinder を適用する

タイトルの通りです、それ以上の情報がないのですが...!下記のドキュメントを参考に、そういうものを作りました。

動機

iOS の Safari から改行を含む <textarea> のデータを POST すると勝手に CR が付与されるという問題がありました。Form の Submit イベントを JavaScript でハンドリングして CR を除外してから送信しても勝手に付与される不思議!なんでだ!

もはやクライアント側では回避不能なのではないかということになり、サーバー側で CR を削除するしかないということで対応することにしました。

実装

ASP.NET Core MVC には (ASP.NET MVC にも) ModelBinder という POST されたデータをプロパティや変数に値を詰める処理をカスタマイズする拡張ポイントがあるので、それを使っています。案外簡単ですね!

public class IgnoreCarriageReturnStringBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
            throw new ArgumentNullException(nameof(bindingContext));

        //--- 値を取り出す
        var modelName = bindingContext.ModelName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
        if (valueProviderResult == ValueProviderResult.None)
            return Task.CompletedTask;

        //--- ModelState を更新
        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        //--- CR を削除
        var value = valueProviderResult.FirstValue;
        if (value != null)
            value = value.Replace("\r", string.Empty);

        //--- バインディング成功
        bindingContext.Result = ModelBindingResult.Success(value);
        return Task.CompletedTask;
    }
}

あとは適用したいプロパティにピンポイントで属性を貼れば OK です。サービス全体で適用する場合は Startup.cs で処理しても良いですが、良し悪しあるので適宜判断してください。

public class HogeModel
{
    //--- textarea とマップされるプロパティ
    [ModelBinder(typeof(IgnoreCarriageReturnStringBinder))]
    public string Text { get; set; }
}

Microsoft Connect(); 2018 Japan で登壇しました & Visual Studio 2019 新機能フォローアップ

Microsoft Connect(); 2018日本版 Wrap Up イベントに参加し、ちょまど (@chomado) ちゃん枠のトップバッターとして Visual Studio 2019 (Preview) の新機能について登壇/解説させていただきました。いろいろドタバタしつつもとても楽しかったですw

イベントは YouTube Live でストリーミング配信されていて、アーカイブも残っているのでいつでも見直すことができます。僕の担当部分は 42:40 あたりから。また、デモで利用していたプロジェクトは GitHub で公開しています。

解説した機能

イベント中に解説したのは以下の機能です。10 分ちょっとのデモにしてはだいぶ詰め込んだ感がありますが、こんなことができるようになったというのを上記動画から少しでも感じていただけていれば幸いです。

  • Look & Feel の変更
  • Document Health Indicator
  • Code Clean-up
  • .editorconfig のエクスポート
  • Convert to LINQ
  • IntelliCode

ちなみに Microsoft 公式の What's New 解説は以下にあります。

解説すれば良かった機能 / し忘れた部分

「やればよかった」「言い忘れた」というものももちろんあります。フォローアップとして以下の 2 つを取り上げて解説します。

IntelliCode の導入方法

デモでは「あたかも IntelliCode が Visual Studio 2019 に標準搭載されている」ような説明をしてしまいました。これは間違い & とても反省していて、IntelliCode は Visual Studio の拡張機能としてインストールする必要があります。

f:id:xin9le:20181223204732p:plain

Marketplace サイトからダウンロードしていただいても大丈夫です。

また IntelliCode は Visual Studio 2019 からの限定機能ではないので Visual Studio 2017 でも利用することができます!業務で Visual Studio 2017 を利用している方は、今すぐ IntelliCode で検索!

デバッガーの強化

Visual Studio 2019 ではデバッグ機能がまたひとつ強化され、ウォッチウィンドウで変数や値の検索ができるようになりました!これを紹介しなかったことを後悔しています。超便利!(語彙力

f:id:xin9le:20181223205411p:plain

これまである時点で特定の変数に特定の値が入っているかどうかを調べようと思ったら、僕の知る範囲では以下の 2 つしか方法がありませんでした。

  1. ウォッチウィンドウで変数を掘り返して特定の値が入っているかを確認する
  2. 条件付き Break Point で止める

Visual Studio 2019 からは Break Point で止めてから検索することができるようになるので、非常に捗るのではないかと思います。

ちょっとした裏話

事前に僕に割り当てられていた時間は 12 分でした。なのですが会場のディスプレイに Mac が軒並み接続できないというトラブル *1 があって、そのせいで開始が 4 分も遅れてしまいました。一応そんなこともあろうかとバックアッププランとしてちょまどちゃんの Surface Pro にもデモ環境を準備しておいてよかった...。

だけどそもそもカツカツの 12 分!という中で 4 分を失ってテンパる僕。普段から日本人らしく (?) 日本語キーボードを使っているのですが、ちょまどちゃんは英語キーボードを使っているのでミスタイプするわするわ...!そしてデモ中に隣でマイクを持ってもらっていたちょまどちゃんには、僕の手が震えていたのをしっかり目撃されていて...w

結果として僕が解説自体に使った時間は 11:30 だったのですが、接続トラブル込みで 15:30 を使ってしまったのでのっけからイベント全体のタイムスケジュールを崩す羽目になってしまい本当に申し訳なく...

関連記事 / スライド

*1:僕だけでなく他の方も接続が不安定だった

AcrInsight - Azure Container Registry のビューワーを公開しました

タイトルの通りですが、Azure Container Registry (= ACR) のリポジトリにあるイメージを閲覧するツールを作成/公開しました。

なんと (?) 4 日前の 2018/12/05 に Microsoft Connect(); 2018 で公開されたばかりの WPF on .NET Core 3.0 でできています!WPF がオープンソースになった記念にやってみたかったんや...w 画面は以下のような感じで、超ピュアな WPF です。

f:id:xin9le:20181209184606p:plain

モチベーション (= ACR の不便なところ)

最近仕事で ASP.NET Core MVC 製の Web サービスを開発しています。ホスティング先は Web Apps for Containers で、コンテナイメージの格納先としては Azure Container Registry を利用しています。やりたいことが一通りできてそれなりに満足してます。なのですが、そんな利用中の ACR にも不便なところが...!

以下は Azure Portal で Azure Container Registry のリポジトリを見ている画面のキャプチャです。

f:id:xin9le:20181209182448p:plain

リポジトリに格納されているコンテナイメージに紐付いているタグの一覧が閲覧できる特に何てことなさそうな画面ですが、よく見ると個々のイメージとタグの関連付けが全く分かりません。イメージとタグの関係は例えば以下のように 1 対多になっていて、Docker コマンドを叩くときもそうですがイメージ ID (= ACR で言うところの Digest) はよく利用します。

イメージ ID (= Digest) タグ
sha256:abc123 build-1024
latest
sha256:xyz789 build-1023

ここで ACR の課金体系について考えてみます。ACR の課金体系は格納しているコンテナイメージの容量に依存していて、プランに応じた一定範囲を超えるとドンドン増えていきます。なので、一定容量の範囲に抑えようと思うのであれば定期的にイメージを削除しなければなりません。

ではどうやってイメージを削除するのかと言うと、現状は Azure CLI を叩くしかありません。Azure Portal 上からはタグの削除はできるのですが、それはイメージについているタグを消しているだけでイメージ自体の削除にはなっていません。つまり、イメージに関連付いているタグをすべて削除してしまうとタグのついていない宙ぶらりんなコンテナイメージが残ってしまうことになります。Portal 上から見えていないのにひっそりと課金対象だと...??

そんなこんな、イメージとタグの関係をしっかりと把握して適切に削除なり操作をしたいという気持ちが強くなりました。Azure CLI を使えばこれらの詳細な情報は見られるのですが、やっぱり視覚化するのが良いなと思ってツールとして作ることにしました。

WPF on .NET Core 3.0 の始め方

まだプロジェクトテンプレートがないのでほんの少しだけ手間ですが、実は全然難しくありません。

  1. Visual Studio 2019 Preview 1 (執筆時点) をインストール
  2. .NET Core 3.0 をインストール
  3. コマンドラインで dotnet new wpf と入力してプロジェクトを作成

この 3 を実行するとき、プロジェクト名のフォルダをカレントディレクトリとして実行してください。そうするとイイ感じにテンプレートからプロジェクトが生成されます。

ACR の .NET SDK

実は ACR のリポジトリ情報を閲覧/操作する .NET SDK は NuGet に公開されていません。おかげで .NET から API を叩くのが地味に厳しい...。なのですが、幸いにも (?) GitHub にまだ作業途中のような感じのプロジェクトがあったので、それを使って C# から REST API を叩いています。

この SDK がまだ閲覧関連しかサポートしていないので、残念ながらイメージの削除を実装できませんでした。これは追々なんとか対応したいなーと思ってはいます。

おまけ

WPF 開発と言えば MVVM (?) ということで ReactiveProperty を使っています。ちゃんと .NET Core 3.0 でも動きます。ぜひ今後ともご贔屓によろしくお願いします :)

Slack.Integration - Slack 連携ライブラリを公開しました

もはや 2 番煎じ、3 番煎じどころか 10 番煎じくらいのものですが、.NET アプリと Slack を繋ぐライブラリ Slack.Integration を作成し、公開しました!現時点では Incoming Webhook *1 にだけ対応していますが、そのうち Slash Command にも対応させる予定です。

どうして車輪の再発明を?

Incoming Webhook において以下の機能を満たしたものが欲しかった、というのが主な理由です。

  • ドキュメントで見た Incoming Webhook のメッセージ投稿の表現を可能な限り使いたい
  • ASP.NET Core MVC 上で扱うときに HttpClientFactory の TypedClient を使った Dependency Injection に準拠したい
  • 標準の絵文字定義を使いたい
  • 特別扱いされている既定のカラー定義を使いたい

また今後 Slash Command を作るときに、同一のライブラリ内に Slash Command の機能も閉じ込めておきたいなぁという思惑もあります。

メッセージ表現の網羅

ドキュメントからの起こし漏れがなければですが、たぶん提供されているすべての表現ができます。たとえば下記のような感じです。REST API で飛ばす必要がある JSON に忠実なプロパティ名なので、困ったとしてもドキュメントとの対比が容易かと思います。

var payload = new Payload
{
    UserName = "Incoming Webhook",
    Text = "Hello, @xin9le !!\n\nThis posts is test for _Incoming WebHook_ .\nDocument is <https://api.slack.com/incoming-webhooks|here>.",
    Markdown = true,
    Channel = "#random",  // override channel
    LinkNames = true,  // linkify names and channels 
    IconEmoji = KnownEmoji.PlusOne,
    Attachments = new []
    {
        new Attachment
        {
            Fallback = "脱・読みづらいコード!今日から一段上のプログラマーになる方法 5 選",
            Color = Color.AliceBlue.ToHex(),
            PreText = "This text is optional that is displayed at the top of _attachment block_ .",
            AuthorName = "xin9le",
            AuthorLink = "http://xin9le.net",
            AuthorIcon = "https://pbs.twimg.com/profile_images/1047114972118114305/vw07RO7H_normal.jpg",
            Title = "脱・読みづらいコード!今日から一段上のプログラマーになる方法 5 選",
            TitleLink = "http://blog.xin9le.net/entry/2016/02/26/043557",
            Text = "「ソースコードを綺麗に書く」というのは、プログラマーであれば誰しもが心掛けたいと思っている *極めて重要な事柄* です。そもそも「綺麗なコードってなんぞ?」という感じですが、いくつかあると思います。",
            ImageUrl = "http://cdn-ak.f.st-hatena.com/images/fotolife/x/xin9le/20160226/20160226040749.png",
            ThumbUrl = "https://pbs.twimg.com/profile_images/1047114972118114305/vw07RO7H_normal.jpg",
            Footer = "xin9le",
            FooterIcon = "https://pbs.twimg.com/profile_images/1047114972118114305/vw07RO7H_normal.jpg",
            Timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(),
            Fields = new []
            {
                new Field
                {
                    Title = "Unique User",
                    Value = "345",
                    Short = true,
                },
                new Field
                {
                    Title = "Page View",
                    Value = "12345",
                    Short = true,
                },
            },
            Actions = new []
            {
                new Slack.Integration.IncomingWebhook.Action
                {
                    Type = ActionType.Button,
                    Text = "GitHub",
                    Url = "https://github.com/xin9le",
                    Style = ActionStyle.Default,
                },
                new Slack.Integration.IncomingWebhook.Action
                {
                    Type = ActionType.Button,
                    Text = "Blog",
                    Url = "https://blog.xin9le.net",
                    Style = ActionStyle.Primary,
                },
                new Slack.Integration.IncomingWebhook.Action
                {
                    Type = ActionType.Button,
                    Text = "Twitter",
                    Url = "https://twitter.com/xin9le",
                    Style = ActionStyle.Danger,
                },
            },
        },
    },
};

表示サンプル

ASP.NET Core MVC の DI に対応

ASP.NET Core MVC 2.1 から HttpClientFactory がサポートされるようになり、HttpClient のインスタンスをフレームワークに管理させつつ効率的に利用することができるようになりました。ASP.NET Core MVC なサービスから直接 Slack にメッセージを飛ばすようなケースでは、自分でインスタンスを作るのではなく先の HttpClientFactory の機構に乗っかるのが良いだろうと思い、それに準拠させられるようにしました。

以下のような感じで DI に登録し、利用することができます。

// Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient<WebhookClient>();  // supports Typed Client pattern
}
// XxxController.cs

public async Task<IActionResult> DoSomethingAction([FromServices] WebhookClient client)
{
    await client.SendAsync(url, payload);
}

ご利用は NuGet から

すでに NuGet からダウンロードして利用することができます。Slack にメッセージを飛ばしたいという方は、ぜひ使ってみてください。お手軽で便利です。

PM> Install-Package Slack.Integration

*1:Slack への投稿

ASP.NET Core MVC における Required 属性と BindRequired 属性の統合

ASP.NET Core MVC に限らず ASP.NET MVC 時代からそうですが、最もよく利用するモデル検証属性として Required 属性があります。Required の名前の通り「入力必須」であることを表すのですが、実際の挙動は非 null の判定を行うものです。

ですので、以下のように int のような値型に対して Required 属性を付与しても効果はありません。これは既定値 0 が設定されるためです。

public class Person
{
    [Required]  // null 以外を強制できる
    public string Name { get; set; }

    [Required]  // これは特に意味がない
    public int Age { get; set; }
}
public IActionResult Post(Person person)
{
    // Age に値が設定されていなくても検証は valid になる
    if (!this.Model.IsValid)
        return this.BadRequest();

    // do something
}

もはや NotNull 属性の方がネーミングとして分かりやすいんじゃないかと思うくらいです。

ASP.NET MVC 時代の解決方法

これの値型の既定値が入ってしまって入力されているのかが分からない問題を解決するため、ASP.NET MVC 時代には Nullable 型 を利用することで回避してきました。

public class Person
{
    [Required]
    public string Name { get; set; }

    [Required]  // int? なら null 以外かどうかで判断できる
    public int? Age { get; set; }
}

ちっぽけな問題かもしれませんが、int として扱いたいのに int? を強要されるのはあまり好ましいことではありません。

ASP.NET Core MVC 時代の解決方法

今知りたいのは null かどうかではなく、値がバインドされたかどうかです。値型に対して値がバインドされたかどうかを判定するための機能として、ASP.NET Core MVC から BindRequired 属性が追加されました。以下のようにすれば、望んだ通り入力必須の検証を行うことができます。これは嬉しい改善です。

public class Person
{
    [Required]
    public string Name { get; set; }

    [BindRequired]  // 値がバインドされることを強制
    public int Age { get; set; }
}

Required 属性と BindRequired 属性を統合する

BindRequired 属性が追加されて願いは叶ったのですが、参照型と値型で 2 種類の属性を使い分けるのはミスも出やすいですし何より分かりにくいです。そこで、以下のようにバインディングをカスタマイズしてしまうことで [Required] 属性に対してもバインディングを必須にしてみます。

public class BindingRequiredMetadataProvider : IBindingMetadataProvider
{
    public void CreateBindingMetadata(BindingMetadataProviderContext context)
    {
        // パフォーマンスを最大化するため敢えて NO LINQ
        for (var i = 0; i < context.Attributes.Count; i++)
        {
            // [Required] が付いていたらモデルバインドを必須にしちゃう
            if (context.Attributes[i] is RequiredAttribute)
            {
                context.BindingMetadata.IsBindingRequired = true;
                return;
            }
        }
    }
}

機能を有効に利用する場合ば Startup.cs の中で以下のように設定します。

services.AddMvc(o =>
{
    o.ModelMetadataDetailsProviders.Add(new BindingRequiredMetadataProvider());
});

こうすることで、冒頭のように [Required] 属性だけで入力必須を表現することができるようになります。便利!

public class Person
{
    [Required]
    public string Name { get; set; }

    [Required]  // これで入力必須にできる
    public int Age { get; set; }
}