1.21 jigowatts

Great Scott!

Visual Studio Application Insights と ライブストリームを試してみた

概要

こちらのde:codeセッションを見ていたところ、Application Insightsという機能が紹介されていました。
channel9.msdn.com

Visual Studio Application Insights は、実行中のアプリケーションを監視し、パフォーマンスの問題や例外の検出と診断、アプリの使用方法の把握に役立ちます。

https://azure.microsoft.com/ja-jp/documentation/articles/app-insights-asp-net/

まだプレビューなのですが、面白そうだったのでアプリケーションに組み込んでAzureのポータルでのライブストリームでの監視をするところまで試してみます!

環境

Visual Studio Community 2015 Update 2
ASP.NET MVC 5
Azure 開発者プログラム特典サブスクリプション

実装

新規プロジェクト作成時にも追加することができますが、今回は既存のプロジェクトに追加します。
Visual Studioでプロジェクトを右クリックし、コンテキストメニューから[Application Insights テレメトリの追加]を選択します。

f:id:sh_yoshida:20160615115638p:plain

アカウント情報とテレメトリの送信先を選択します。
新規でも作成できますが、既存のリソースに追加してみました。大きなアプリケーションの場合は他のコンポーネントと同じリソースグループに配置することがお勧めらしいです。

f:id:sh_yoshida:20160615115645p:plain

この状態でデバッグしても動くのですが、Application Insights Web SDKを最新の安定版に更新するようお勧めされるのでNuGetパッケージマネージャで[Microsoft.ApplicationInsights.Web]を最新プレビューに更新しました。

f:id:sh_yoshida:20160615115653p:plain

ローカル環境でデバッグ実行するとVisual Studioの診断ツールや、Application Insightsの検索ウィンドウでアプリケーションの状態を観測できます。先日作ったWeb APIの動作の詳細も確認できました!

f:id:sh_yoshida:20160615115726p:plain

また、プロジェクトを右クリックし、コンテキストメニューから[Application Insights]-[Open Application Insights Portal]を選択することでAzureのポータルが開きます。

f:id:sh_yoshida:20160615124752p:plain

ここでもアプリケーションの状態を監視することができ、さらにライブストリームではほぼリアルタイムで状態を観測できるようです。

f:id:sh_yoshida:20160615125037p:plain

試しに一人でリクエスト投げたり、エラー発生させてみたりしてみましたが、ほぼリアルタイムでした。

f:id:sh_yoshida:20160615120830g:plain

このライブストリーム機能はMicrosoft.ApplicationInsights.Webの2.1.0-beta2以降が必要です。上記の通り、デフォルトのバージョンが1.2.3だったため更新するまで使えませんでした。
あとこのWeb SDKは現段階では.NET Framework 4/4.5に依存するため、ASP.NET Coreプロジェクトではダメでした。

ふわーっと触って満足してGitHubにコミットしようと思ったのですが、.csprojファイルにサブスクリプションIDが記述されてますね。
f:id:sh_yoshida:20160615130836p:plain

サブスクリプション ID とは

Windows Azure をご利用頂く際に、お客様と日本マイクロソフト株式会社は、無償または有償のサブスクリプション契約 (無料評価版、従量課金プラン等) を結びます。お客様が締結したこのサブスクリプション契約を一意のものとして識別するために、マイクロソフトでは 32 桁の GUID を付与しています。この GUID がサブスクリプション ID です。

https://blogs.msdn.microsoft.com/dsazurejp/2013/12/06/id-id/

うーん、これは公開しないほうがいいかも?

ASP.NET MVC オートコンプリート

概要

あると便利!jQuery UIのAutocomplete(オートコンプリート)をASP.NET MVCで実装してみます。
今回のサンプルは入力した文字列に応じて入力候補が表示され、選択した値が各項目にセットされるというものです。

f:id:sh_yoshida:20160612013611g:plain

環境

Visual Studio Community 2015 Update 2
ASP.NET MVC5
jQuery UI

実装

コントローラー

必要なのはJSONデータなので、JsonメソッドによりJsonResultオブジェクトを返します。
既定ではHTTP GETリクエストではJSONデータを返せないようです。
Controllers\HomeController.cs

//ビューの表示
public ActionResult Create()
{
    return View();
}

public JsonResult AutoComplete(String name)
{
    var data = _service.GetPeopleByName(name);
    return Json(data);
}

明示的にGETリクエストを許可するには以下のようにする必要があります。

return Json(data, JsonRequestBehavior.AllowGet);

ワーカーサービス

多くの場合、リポジトリクラスからデータベースにアクセスし必要なデータを取得すると思いますが、今回はサンプルのため擬似コードで割愛。
Services\People\PeopleService.cs

public class PeopleService : IPeopleService
{
    public IEnumerable<Person> GetPeopleByName(string name)
    {
        var data = GetData();
        var result = data.Where(c => c.Name.StartsWith(name, ignoreCase: true, culture: ci));

        return result;
    }
}

モデル

Personクラスをベースにします。
Models\Person.cs

public class Person
{
    public int Id { get; set; }
    public String Name { get; set; }
    public String Email { get; set; }
    public String Birthday { get; set; }
}

ビュー

簡単な入力フォーム上でName項目に入力した値で動作するようにしています。
Views\Home\Create.cshtml

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Person</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Birthday, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Birthday, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Birthday, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

JavaScript

$(function () {
    $('#Name').autocomplete({
        source: function (request, response) {
            var param = { name: $('#Name').val() };

            $.ajax({
                url: '@Url.Content("~/Home/AutoComplete")',
                data: JSON.stringify(param),
                datatype: "json",
                type: "POST",
                contentType: "application/json",
                dataFilter: function (data, type) { return data },
                success: function (data, dataType) {
                    response($.map(data, function (item) {
                        return {
                            label: "ID:" + item.Id + " Name:" + item.Name + " Email:" + item.Email + " Birthday:" + item.Birthday,
                            value: item.Name,
                            extend: item
                        };
                    }));
                },
                error: function (XMLHttpRequest, textStatus, errorThrown) {
                    alert('error');
                }
            });
        },
        select: function (event, ui) {
            $('#Email').val(ui.item.extend.Email);
            $('#Birthday').val(ui.item.extend.Birthday);

        }
    });
});

実行結果

Name項目に入力した文字列に対して入力候補が絞られていきます。
最終的に選択した値が各項目にセットされました!
f:id:sh_yoshida:20160612013611g:plain

ASP.NET Web APIという選択

ASP.NET MVCはRPCベースのスタイルであるのに対し、ASP.NET Web APIはRESTfullスタイルが既定(RPCもイケル)です。
ASP.NET MVCではJSONデータが返せるため、あまりメリットを感じないのですが、本来はWeb APIとして提供した方がいいケースもあります。

上記のコードを少し変更しつつ、ASP.NET Web APIバージョンも実装してみます。

実装

APIコントローラー

MVC6では統合されたようですが、今回はMVC5なのでASP.NET Web APIコントローラーはまだSystem.Web.HttpアセンブリのApiControllerを継承しています。
Api\PeopleController.cs

public class PeopleController : ApiController
{
    public IEnumerable<Person> GetByName(String name)
    {
        return _service.GetPeopleByName(name);
    }
}

プロジェクト作成時にテンプレートでWeb APIを選択していない場合、APIコントローラーを追加しただけでは動かないため、以下を追加します。

既定のルートの設定

App_Start\WebApiConfig.cs

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}
Web APIの既定ルートを登録

Global.asax

protected void Application_Start()
{
    //追記
    GlobalConfiguration.Configure(WebApiConfig.Register);
    ...
}

ビュー

GETリクエストでhttp://{server}/api/{apicontroller}へアクセスしJSONデータを取得します。
JavaScript

$(function () {
    $('#Name').autocomplete({
        source: function (request, response) {
            var param = { name: $('#Name').val() };

            $.getJSON('@Url.Content("~/api/people")', param)
            .done(function (data) {
                response($.map(data, function (item) {
                    return {
                        label: "ID:" + item.Id + " Name:" + item.Name + " Email:" + item.Email + " Birthday:" + item.Birthday,
                        value: item.Name,
                        extend: item
                    };
                }));
            })
            .fail(function (jqXHR, textStatus, err) {
                alert('error : ' + err);
            });
        },
        select: function (event, ui) {
            $('#Email').val(ui.item.extend.Email);
            $('#Birthday').val(ui.item.extend.Birthday);
        }
    });
});

まとめ

見た目は同じですが、実装をASP.NET Web APIに変更してみました。
どちらを選択するかは仕様によると思いますが、MVC6で統合された経緯を見る限りどちらも使いこなせたほうが良さそうですね。
ASP.NET Coreを視野に入れつつASP.NET Web APIも学習してみようっと。

参考

プログラミングASP.NET MVC 第3版 ASP.NET MVC 5対応版

プログラミングASP.NET MVC 第3版 ASP.NET MVC 5対応版

.NET開発テクノロジ入門 2014年版 VisualStudio2013対応版 (MSDNプログラミングシリーズ)

.NET開発テクノロジ入門 2014年版 VisualStudio2013対応版 (MSDNプログラミングシリーズ)

ASP.NET MVC ファイルアップロード

概要

久しぶりにASP.NET MVCです。以前いくつかMVC2についてポストしましたが、いい加減Razor使いたい!ということでASP.NET MVC5にしてみました。まったく仕事でMVC5を使う予定はありませんがね!

それはさておき、ファイルのアップロードです。最初に完成イメージ。
f:id:sh_yoshida:20160607211237p:plain

環境

Visual Studio Community 2015 Update 2
ASP.NET MVC5

実装

アップロードする画面と結果を表示する画面に対応したコントローラー、モデル、ビューで構成されています。

コントローラー

ファイルをアップロード後にファイル一覧を表示する画面にリダイレクトしています。
Controllers\HomeController.cs

[HttpGet]
public ActionResult Upload()
{
    return View();
}

[HttpPost]
public ActionResult Upload(UserData inputModel)
{
    var destinationFolder = Server.MapPath("~/Users");

    if (!Directory.Exists(destinationFolder))
    {
        Directory.CreateDirectory(destinationFolder);
    }

    var postedFile = inputModel.Picture;
    if (postedFile.ContentLength > 0)
    {
        var fileName = Path.GetFileName(postedFile.FileName);
        var path = Path.Combine(destinationFolder, fileName);

        postedFile.SaveAs(path);
    }

    return RedirectToAction("FileIndex", "Home");
}

[HttpGet]
public ActionResult FileIndex()
{
    var destinationFolder = Server.MapPath("~/Users");
    string[] files = Directory.GetFiles(destinationFolder, "*", SearchOption.AllDirectories);
    ViewBag.files = files;

    return View();
}

モデル

アップロードファイルのモデルバインディングに対応するにはHttpPostedFileBase型として宣言する必要があります。
Models\UserData.cs

public class UserData
{
    public string Name { get; set; }
    public string Email { get; set; }
    public HttpPostedFileBase Picture { get; set; }
}

ビュー

(念願のRazorです!)
ファイルアップロードするにはformタグにenctype = "multipart/form-data"が必要です。
Views\Home\Upload.cshtml

@using (Html.BeginForm("Upload", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>UserData</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Picture, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                <input type="file" id="picture" name="picture" class="form-control" />
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

結果はViewBagに入れて表示するだけ。
Views\Home\FileIndex.cshtml

@foreach (var file in ViewBag.files)
{
    <div>@file</div>
}

実行結果

Pictureを選択して[Create]ボタンを押下すると…

f:id:sh_yoshida:20160607211237p:plain

保存されました!これはAzure App ServiceのWebサイトにアップした結果です。こんなパスになるんですね。

f:id:sh_yoshida:20160607231524p:plain

Kuduでも確認してみました(初めて使ってみる)。ちゃんと保存されているようです。

f:id:sh_yoshida:20160607231532p:plain

実際にはセキュリティだとかエラーハンドリングだとかもっとしっかりやらないとダメですよ!(‘А`)

Server.MapPathについて

参考書には以下のように指定されてまして、この通りでもローカルで実行した場合はうまく動きました。

Server.MapPath("/Users")

ただ、IISにデプロイするときにアプリケーション名(このサンプルではvoyager02)を指定するとパスが通らなくなってしまいました。
f:id:sh_yoshida:20160607235039p:plain

これは/(スラッシュ)を指定するとWebサイト(ドメインのルート)の物理パスを取得するため一個上の階層になってるんですね。また、ドメインのルートとアプリケーションのルートは必ずしも階層関係にある必要はないため余計にややこしいですね。

f:id:sh_yoshida:20160607235053p:plain

Webサイト直下に配置するのであればスラッシュでもいいのかもしれませんが、ルート演算子(~)をつけてあげるとアプリケーションルートの物理パスが取れるので、アプリケーション名に依存しなくなります。

Server.MapPath("~/Users")

参考

stackoverflow.com

プログラミングASP.NET MVC 第3版 ASP.NET MVC 5対応版

プログラミングASP.NET MVC 第3版 ASP.NET MVC 5対応版