ぷろじぇくと、みすじら。

ASP.NET MVC Core 2.0で作ったAPIでXMLなどの形式を返したい

Created at:

ASP.NET MVC Coreはアクションの戻り値が IActionResult ではない場合、デフォルトでJSONをシリアライズしたものを返します。しかし場合によってはJSONだけでなくXMLで返したいという場合もあるかと思います。

そのようなニーズにこたえるべくフォーマッターを登録することで様々な形式でレスポンスを返すことができる仕組みがあります。ここではXMLで返す方法を説明します。

まずXML用のフォーマッターはNuGetから Microsoft.AspNetCore.Mvc.Formatters.Xml パッケージをインストールできるのでインストールします。

次にConfigureServiceメソッドのASP.NET MVC Coreの設定でXMLのフォーマッターを登録します。

services.AddMvc(options =>
{
    // XMLを返すフォーマッターとそのMIME Typeと拡張子のマッピングを登録
    // XmlDataContractSerializerOutputFormatterはASP.NET Web API相当
    options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
    // options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
    options.FormatterMappings.SetMediaTypeMappingForFormat("xml", "application/xml");
});

この状態で Accept ヘッダーフィールドに application/xml を指定してリクエストするとXMLでレスポンスが返ってくるようになります。簡単ですね。

XMLシリアライザを使うときの注意

XmlDataContractSerializerOutputFormatterでシリアライズするクラスは引数なしのコンストラクタやプロパティにsetterが必要だったりするのでご注意ください。

この辺りを忘れていると406 Not Acceptableが返ってきて悩むことになります。

URLの .json/.xml のような拡張子で出し分けたい

通常は Accept によるコンテントネゴシエーションで返すフォーマットを決定すればよいのですが、URLの一部でフォーマットを指定したいというということもあります。例えば、/People/Alice.jsonとアクセスした場合はJSONで、/People/Alice.xmlであればXMLを返すといった形です。

これを行うには FormatFilter フィルター(FormatFilterAttributeクラス)をコントローラーにつけ、ルーティングに{format}を足します。例えば以下のようになります。

[FormatFilter]
[Route("[controller]/{id}.{format?}")]
public class PeopleController : Controller
{
    [HttpGet]
    public Person Get(string id)
    {
        return /* Person のインスタンスを取得 */;
    }
}

これでフォーマットが指定された場合はそれにマッチするフォーマッターが、指定されていなければデフォルトのフォーマッターが使用されます。

$ curl http://localhost:4743/People/Alice.json
{"name":"Alice","age":17}

$ curl http://localhost:4743/People/Alice.xml
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/WebApplication19.Controllers"><Age>17</Age><Name>Alice</Name></Person>

デフォルトのフォーマッターを変更する

Acceptやフォーマット名(拡張子)によってフォーマッターを決定できない場合、フォーマッターの登録順で見て行って一番最初に利用できるもの(=必ずしも一番最初ではない)がデフォルトのフォーマッターとなります。

つまり標準では JsonOutputFormatter が選択されてJSONが返されるわけですが、デフォルトをXMLにしたいという場合もあるかもしれません。そのような場合は一度 JsonOutputFormatter を抜いて、XMLのフォーマッターを登録してから再度 JsonOutputFormatter を登録します。

var jsonOutputFormatter = options.OutputFormatters.OfType<JsonOutputFormatter>().First();
options.OutputFormatters.RemoveType<JsonOutputFormatter>();

options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());

options.OutputFormatters.Add(jsonOutputFormatter);

MessagePack-CSharpを使ってみる

折角なのでレスポンスでMessagePackを返すのも試してみましょう。まあWeb APIでMessagePackを受けたり返したりしたいシーンがどのぐらいあるのかは謎といえば謎なのですが。

C#界最速のシリアライザであるところのMessagePack-CSharpにはASP.NET MVC Core用のフォーマッターが用意されているので使うのは簡単です。

まずNuGetからMessagePackMessagePack.AspNetCoreMvcFormatterをインストールします。

インストールできたらXMLの時と同様に登録します。MessagePackのMIME Typeはapplication/x-msgpackなので拡張子をマッピングを登録する場合やリクエストのAcceptにはそれを指定してください。折角なのでInputFormatterも登録しておきます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.FormatterMappings.SetMediaTypeMappingForFormat("msgpack", "application/x-msgpack");
        options.OutputFormatters.Add(new MessagePackOutputFormatter());
        options.InputFormatters.Add(new MessagePackInputFormatter());
    });
}

あとは適当なコントローラーとデータを作ります。ここでは /People/Alice といった形でアクセスできるようなものを作ってみます。

レスポンスに使う Person クラスはMessagePackでシリアライズできるように MessagePackObject としてマークしておきます。これはMessagePack-CSharpのお約束です。

[FormatFilter]
[Route("[controller]/{id}.{format?}")]
public class PeopleController : Controller
{
    [HttpGet]
    public Person Get(string id)
    {
        switch (id)
        {
            case "Alice": return new Person("Alice", 17);
            case "Karen": return new Person("Karen", 17);
            default: return null;
        }
    }
}

[MessagePackObject]
public class Person
{
    [Key(0)]
    public string Name { get; }
    [Key(1)]
    public int Age { get; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

あとは /People/Alice.msgpack または Accept: application/x-msgpack 付きででリクエストを投げることでMessagePackで返ってくるはずです。

$ curl -H 'Accept: application/json' http://localhost:4743/People/Alice 2>/dev/null | od -c
0000000   {   "   n   a   m   e   "   :   "   A   l   i   c   e   "   ,
0000020   "   a   g   e   "   :   1   7   }
0000031

$ curl -H 'Accept: application/x-msgpack' http://localhost:4743/People/Alice 2>/dev/null | od -c
0000000 222 245   A   l   i   c   e 021
0000010

ところでMessagePackObjectとかつけるの面倒だなーMapモードでいいんだけどなーということもあるかもしれません。そんな時はContractlessStandardResolverを使用するとよいでしょう。

services.AddMvc(options =>
{
    options.FormatterMappings.SetMediaTypeMappingForFormat("msgpack", "application/x-msgpack");
    options.OutputFormatters.Add(new MessagePackOutputFormatter(ContractlessStandardResolver.Instance));
    options.InputFormatters.Add(new MessagePackInputFormatter(ContractlessStandardResolver.Instance));
});
public class Person
{
    public string Name { get; }
    public int Age { get; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}
$ curl -H 'Accept: application/x-msgpack' http://localhost:4743/People/Alice 2>/dev/null | od -c
0000000 202 244   N   a   m   e 245   A   l   i   c   e 243   A   g   e
0000020 021
0000021

このように簡単にMessagePackで返せるようになるので使えそうなときは積極的に使っていきたいですね。

余談: MvcOptions.RespectBrowserAcceptHeader とは何か

ASP.NET MVC CoreのMvcOptionsにはフォーマッターに関連したRespectBrowserAcceptHeader プロパティという設定が存在します。この設定は何をするものかというと「ブラウザのようにAcceptに*/*といったワイルドカードのメディアタイプを乗せて来た場合、デフォルトとするかどうか」です。

例えばそれぞれの設定時には以下のようになります。

つまりfalseの場合には*/*text/*のようなものを見つけると、Acceptで指定されたものを全て無視するのと同じになります。基本的にワイルドカードを指定してくるのはブラウザぐらいなのでブラウザのAcceptに敬意を払うかどうかということです。

このプロパティはデフォルトは false となっていますので、もしブラウザのようなAcceptも受け入れてその通りに判断してほしいというときは true を設定してください。

コマンドプロンプトの新しいカラースキームを設定する

Created at:

Windows 10 Fall Creators Updateからコマンドプロンプトのカラースキームが新しくなるという発表があり、すでにInsider Preview Build 16257以降ではデフォルトで新しいカラースキームになっています。

しかし先のエントリーにもあるのですが If you clean-install a new build of Windows 10 >= 16257, you'll get the new colors as the default Console scheme. ということでクリーンインストールしたとき以外は既存のカラースキームのままですよということになっています。

幸いエントリーには新しいカラースキームのカラーコードが乗っているので、手でちくちくと設定すればCreators Update以降であれば再現可能です。とはいえやってみるとわかりますが割となかなか面倒です。エントリーをよく読むと We'll soon be publishing a tool that will help you apply this new scheme and a selection of alternative color schemes to your Windows Console. とあり、その後設定ツールがリリースされました。

ColorTool

GitHubのMicrosoft/consoleColorToolというツールが放流されています。このツールはカラースキームをコマンド一発で設定ファイルから読み込んで設定するツールです。

とりあえず使ってみるにはReleasesにColor Tool Initial Releaseのようにコンパイル済みのもの置かれているのでこちらをダウンロードするのがお勧めです(もしかしたら最新は更新されているかもしれません)。

現在のカラースキームを確認

ダウンロードしたzipを展開するとcolortool.exeというツールが出てくるので、まずは現在の設定状態を表示してみましょう。-c オプションを付けて実行すると現在のカラースキームでプレビューが表示されます。

C:\> colortool -c

カラースキームを設定する

現在のカラースキームを確認したところでカラースキームを変更するには colortool.exe にオプションなしでスキーム名を指定します。指定できるカラースキームは schemes フォルダにある ini ファイルまたは plist ファイルの名前(拡張子なし)となっています。

C:\> colortool campbell

ここで設定したものはこのコンソールのセッションに適用されるものなので、全く新しいセッションを始めた時や別な設定を持つショートカットから起動すると元のままになります。

設定を保存する

ColorToolには設定を永続化する機能も用意されています。

とりあえずは -b オプションを付けてデフォルトの設定を変更しておくとよいかと思います。

C:\> colortool -b campbell

カラースキームいろいろ

同梱されているカラースキームの他にもiTerm向けのカラースキームを使えるようになっているので、iTerm2-Color-Schemeからダウンロードしてきて適用することもできます。というか、同梱されているOneHalfやsolarizedはiTerm形式です。

campbell

campbell-legacy

deuteranopia

OneHalfDark

OneHalfLight

solarized_dark

solarized_light

UWP API経由でBluetooth LEの通知(Notify)をデスクトップアプリで受け取れない問題

Created at:

以前、CC2650STK SensorTagをUniversal Windows Platform APIから使うというエントリを書いたのですが、最近Creators Update以降(というかInsider Preview)で実行したところ通知が動かなくなっていました。

UWPアプリからは問題なく動くのですが、UWP APIを使う通常のいわゆるクラシックデスクトップアプリからはRead/Writeは正常に行えるもののデバイス側からNotify(通知)を受け取ることができなくなっていていました。

要するにコンソールアプリなどでは GattCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify) を呼び出したにも関わらず ValueChanged イベントが発生しないという状況です。

解決方法

問題の解決方法ですが起動時に CoInitializeSecurity を呼び出すというおまじないを書くと動くようになります。

CoInitializeSecurity(IntPtr.Zero, -1, IntPtr.Zero, IntPtr.Zero, RpcAuthnLevel.Default, RpcImpLevel.Identify, IntPtr.Zero, EoAuthnCap.None, IntPtr.Zero);
[DllImport("ole32.dll")]
static extern int CoInitializeSecurity(IntPtr pVoid, int
    cAuthSvc, IntPtr asAuthSvc, IntPtr pReserved1, RpcAuthnLevel level,
    RpcImpLevel impers, IntPtr pAuthList, EoAuthnCap dwCapabilities, IntPtr
    pReserved3);

public enum RpcAuthnLevel
{
    Default = 0,
    None = 1,
    Connect = 2,
    Call = 3,
    Pkt = 4,
    PktIntegrity = 5,
    PktPrivacy = 6
}

public enum RpcImpLevel
{
    Default = 0,
    Anonymous = 1,
    Identify = 2,
    Impersonate = 3,
    Delegate = 4
}

public enum EoAuthnCap
{
    None = 0x00,
    MutualAuth = 0x01,
    StaticCloaking = 0x20,
    DynamicCloaking = 0x40,
    AnyAuthority = 0x80,
    MakeFullSIC = 0x100,
    Default = 0x800,
    SecureRefs = 0x02,
    AccessControl = 0x04,
    AppID = 0x08,
    Dynamic = 0x10,
    RequireFullSIC = 0x200,
    AutoImpersonate = 0x400,
    NoCustomMarshal = 0x2000,
    DisableAAA = 0x1000
}

あまり真面目に追っていないのでおまじない感がすごいですが一応通知が来るようになりました。ちなみにLINQPadからの実行の場合にはうまくいかない可能性が高いです(多分Windows Runtimeの初期化が先に走ってしまったり、再利用されたりなど)。

Windows 10 Insider Preview Build 16273以降

と、調べて解決して、このエントリーを書いた今日、Insider Previewの新しいビルドが来たので更新したところなんと CoInitializeSecurity 呼ばなくても動くように直っていました…oh…。

とりあえず何事もなく動くようになったのでよかったよかったということで…。

Visual Studio 2017のアップデートや変更でエラーが発生する

Created at:

以前から手元の環境でVisual Studio 2017をアップデートすると一部のコンポーネントのインストールに失敗するということが起きていて、一部の機能が正常に動作しないのでなんとかした話です。

まずエラーの発生時にメッセージを見るとMicrosoft.VisualCpp.CRT.Appx.Msi,version=14.10.25506.0のインストールに失敗していることが分かりました。

Package 'Microsoft.VisualCpp.CRT.Appx.Msi,version=14.10.25506.0' failed to install.
    Search URL
        https://aka.ms/VSSetupErrorReports?q=PackageId=Microsoft.VisualCpp.CRT.Appx.Msi;PackageAction=Install;ReturnCode=1603
    Details
        MSI: C:\ProgramData\Microsoft\VisualStudio\Packages\Microsoft.VisualCpp.CRT.Appx.Msi,version=14.10.25506.0\VC_CRT.Appx.msi, Properties:  REBOOT=ReallySuppress ARPSYSTEMCOMPONENT=1  MSIFASTINSTALL="7"  VSEXTUI="1"  VS7.3643236F_FC70_11D3_A536_0090278A1BB8="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise" 
        Return code: 1603
        Return code details: Fatal error during installation.
    Log
        C:\Users\Tomoyo\AppData\Local\Temp\dd_setup_20170819173137_003_Microsoft.VisualCpp.CRT.Appx.Msi.log
    Impacted workloads
        Universal Windows Platform development (Microsoft.VisualStudio.Workload.Universal,version=15.0.26720.2)
    Impacted components
        Universal Windows Platform tools (Microsoft.VisualStudio.Component.UWP.Support,version=15.0.26621.2)
        Universal Windows Platform tools for Cordova (Microsoft.VisualStudio.ComponentGroup.UWP.Cordova,version=15.0.26711.1)
        Universal Windows Platform tools for Xamarin (Microsoft.VisualStudio.ComponentGroup.UWP.Xamarin,version=15.0.26606.0)

さらにメッセージに書かれているログファイルを見てみると以下のようなメッセージが残っていました。

MSI (s) (2C:68) [17:32:18:305]: Product: Visual C++ Library CRT Appx Package -- Error 1714. The older version of Visual C++ Library CRT Appx Package cannot be removed.  Contact your technical support group.  System Error 1612.

どうやら旧バージョンの"Visual C++ Library CRT Appx Package"のアンインストールに失敗しているようです。よく見るとそのログの少し上にWarning: Local cached package 'C:\WINDOWS\Installer\68f56c6.msi' is missing. とありVisual C++ Library CRT Appx Packageのインストール元となったmsiのキャッシュがないといったメッセージも残っています。

強制的にパッケージをアンインストールする

ともあれこのパッケージが悪そうなのでアンインストールしますが、通常の手順ではアンインストールできないので少し手間をかける必要があります。

パッケージのIDを調べる

まずはアンインストールするパッケージのIDを調べる必要があります。そのためのツールをMicrosoftの人がUsing MsiInv to gather information about what is installed on a computerというエントリで公開しているのでこれをダウンロードします。

このツールをダウンロードしてエントリにあるように以下のように実行してファイルに書き出します。

.\msiinv.exe -p > output.txt

書き出したらその結果から "Visual C++ Library CRT Appx Package" で "Local package" が上記ログにあった見つからないファイルである "68f56c6.msi" (環境によっては異なる) となっているものを探し、そのProduct codeをメモしておきます。

修復ツールでアンインストールする

次にMicrosoftのサポートページからインストール/アンインストールの修復ツールをダウンロードします。

ダウンロードしたものを開くとトラブルシューターが起動するので、問題の種類として"Uninstalling"を選択します。しばらくするとインストール済みのパッケージの一覧が表示されます。

パッケージ名にマウスカーソルをホバーするとGUID的なものが表示されるので、先ほどのProduct codeとおなじものを探し出し、選択して、ウィザードを続けます。しばらくまってアンインストールが完了すれば出来上がりです。

あとはVisual Studioのアップデートや変更でエラーが発生しないことを確認できればめでたしめでたしです。

App Service PlanとApp Serviceをリソースグループ間で移動する場合の制限

Created at:

AzureのApp Service PlanとApp Serviceをリソースグループ間で移動する場合には制限事項があり、その手順に沿って移動しないとうまくいきません。…というのにはまりました。

ドキュメントのApp Serviceの制限事項には以下のように書かれています。

App Service アプリを使用している場合、App Service プランのみを移動することはできません。 App Service アプリを移動するには、次のオプションがあります。

  • App Service プランとそのリソース グループ内の他のすべての App Service リソースを、まだ App Service リソースが含まれていない新しいリソース グループに移動する。 この要件により、App Service プランに関連付けられていない App Service リソースも移動する必要があります。
  • アプリを別のリソース グループに移動し、元のリソース グループにも App Service プランをすべて保持する。

アプリが正常に動作するために、App Service プランがそのアプリと同じリソース グループ内に存在する必要はありません。

これ(とそれに続く説明)はどういうことなのかというのがピンとこないので順を追って移動する例を見ていきます。

初期構成の想定

まずは初期構成として以下のような構成になっているとします。

最終的な構成

次に移動した結果として希望したい構成です。

Step 1: 新しいグループを作ってAppServiceとともに移動する

まずは新しいリソースグループ BasicPlan を作って、App Service Planを移動します。この際、App Service Planの移動とそれに付随するリソース(App Serviceなど)はすべて新しいリソースグループに移動する必要があります。

Step 2: 他のリソースも新しいグループに一度まとめる

次にもう一つのApp Serviceも一度新しいリソースグループ BasicPlan に移動します。 App Service自体の移動にはApp Service Planは必要ないのでそのまま移動できます。

Step 3: App Serviceを移動する

次に新しいリソースグループ App1Group, App2Group を作り、そこにApp Serviceのみを移動します。

Step 4: App Service以外を移動する

最後にApp Service以外を移動します。

注意事項

az resource move (azure-cli)で移動する

Azure ポータルからではなくazure-cliを利用して移動すると、バリデーションがかからずバラバラに移動できてしまいポータルよりいいと思いきや、想定外の形になることがあるようなので素直にポータルからやっていきましょう。

原則として1つのResource Groupに一つ以上WebApp リソース(App Service Plan)を移動できない

App Service Planの移動先にすでにMicrosoft.Web以下のリソースタイプなどがあると移動できません。ドキュメントのまだ App Service リソースが含まれていない新しいリソース グループに移動するというのはこの制限事項のことを指しています。

この状態で移動しようとすると以下のようなエラーメッセージが表示されます。

There was an error moving resources. Resource move validation failed. Please see details. Diagnostic information: timestamp '20170805T164541Z', subscription id 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', tracking id 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', request correlation id 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'. (Code: ResourceMoveProviderValidationFailed) Cannot move WebApp resources to a resource group that already contains WebApp resources. Please ensure destination resource group Common does not have any WebApp resources in it before performing the next move operation. Or create a new resource group and move resources there. (Code: BadRequest, Target: Microsoft.Web/serverFarms)

App Service Planの移動と同時にすでにバラバラになっているApp Serviceを一度移動する必要がある

App Service Planを移動するときには関連したApp Serviceも同時に移動する必要があります。つまり一度App Service Planを移動する前にApp Serviceを同じリソースグループに移動する必要があります。

ドキュメントではWeb アプリがその App Service プランとは異なるリソース グループに存在するが、その両方を新しいリソース グループに移動する場合、移動を 2 段階で行う必要がありますとして解説されているのがこれです。

この状態で移動しようとすると以下のようなエラーメッセージが表示されます。

There was an error moving resources. Resource move validation failed. Please see details. Diagnostic information: timestamp '20170805T175159Z', subscription id 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', tracking id 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', request correlation id 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'. (Code: ResourceMoveProviderValidationFailed) Cannot move resources because some site(s) are located in other resource group(s) but hosted by resource group 'Default-Web-JapanEast'. The list of site(s) and corresponding resource groups: 'website-01:Default-Web-JapanEast,website-02:Default-Web-JapanEast'. This may be a result of prior move operations. Move the site(s) back to respective hosting resource group(s) and try again. (Code: BadRequest, Target: Microsoft.Web/serverFarms)

Microsoft.Web/certificates リソースタイプは移動不能

証明書はApp Service Planに関連付けられ、そのリソースグループに移動不能な形で残るので最悪削除する羽目になります(新しいリソースグループで証明書を追加できなくなる場合がある)。

まとめ

移動するのは手順はともかくとしても処理に時間がかかるので、App Service Planとリソースグループの構成は最初からちゃんと考えておいたほうがいいでしょう。まあ真面目に使うときには考えて作るとは思いますが…。