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

ASP.NET MVC Core 2.0のアプリケーション全体でHTTPSを強制する

Created at: | Tag: ASP.NET

ASP.NET MVC Core 2.0のアプリケーションでHTTPSを強制するには、RequireHttpsフィルター(RequireHttpsAttributeクラス)をグローバルフィルターに追加することで実現できます。

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services
        .AddMvc(options =>
        {
            options.Filters.Add(new RequireHttpsAttribute());
        });
}

注意点として、RequireHttpsAttributeフィルターはOnAuthorizationメソッド、つまりフィルターパイプラインの一番最初である認可フェーズで処理されるので、ほかの認証/認可フィルターはこのフィルターより後に登録する必要があります。

MicrosoftのサイトにはRewriteエンジンを使う方法も書かれていますが、大抵のケースではフィルターで追加する方法で間に合うと思います。

MVCを使わないASP.NET Coreでもリダイレクトしたい

また、MVCではないASP.NET Coreで同じようなことをしたい場合には以下のようなMiddlewareを書くとよいでしょう(動作未確認)。

public static IApplicationBuilder UseRedirectToHttps(this IApplicationBuilder app)
{
    return app.Use((context, next) =>
    {
        var req = context.Request;
        if (!req.IsHttps)
        {
            var newUrl = new StringBuilder().Append("https://").Append(req.Host.Host).Append(req.PathBase).Append(req.Path).Append(req.QueryString);
            context.Response.Redirect(newUrl.ToString(), permanent: true);
            return Task.CompletedTask;
        }

        return next();
    });
}

IHostingEnvironmentをConfigureServiceで使いたい

ところでグローバルなフィルターを追加するのはいいのですが、環境によっては例えばデバッグ実行時や開発環境では追加したくないということもあります。

Configure メソッド内であれば env.IsProduction() といった感じで切り替えできるのですが、ConfigureService メソッドには IHostingEnvironment が渡ってこないのです。

ではどうするかというと Startup クラスにプロパティやフィールドをはやして、保持しておくという方法で解決できます。まあ Configuration も保持してますしね。

public Startup(IHostingEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
        .AddEnvironmentVariables();
    Configuration = builder.Build();
    HostingEnvironment = env;
}

public IHostingEnvironment HostingEnvironment { get; }

一度保持してしまえば ConfigureServices から参照可能になるので環境ごとに切り替えができます。

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services
        .AddMvc(options =>
        {
            if (HostingEnvironment.IsProduction())
            {
                options.Filters.Add(new RequireHttpsAttribute());
            }
        });
}

ASP.NET MVC Core 2.0が返すJSONをASP.NET Web APIと同様にPascal Caseにしたい

Created at: | Tag: C# .NET ASP.NET

ASP.NET MVC CoreのJSONシリアライズではプロパティ名をデフォルトで"camelCase"に変換して吐き出しますが、従来のASP.NET Web APIは"PascalCase"だったので移植する場合などに以前の挙動になってほしいということがあります。

幸いシリアライザの挙動を変更する手段が用意されているのでそこで変更できます。

StartupConfigureServicesでASP.NET MVCを利用するためAddMvcメソッドを呼び出しますが、その際に返ってきたIMvcBuilderAddJsonOptionsを呼び出すことで設定を変更できます。

ContractResolverというプロパティがシリアライズするときに使うリゾルバなのでこれを変更します。従来のASP.NET Web API相当のシリアライザはJSON.NETのデフォルトなのでDefaultContractResolverContractResolverとして設定すれば期待の挙動となります。

services
    .AddMvc()
    .AddJsonOptions(options =>
    {
        options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver();
    });

他にもJSONのシリアライズをカスタマイズしたい場合にはこの辺りで変更を加えたり、ContractResolverをカスタムしてあげればよいということになります。

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

Created at: | Tag: C# .NET ASP.NET

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: | Tag: Windows

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: | Tag: C# UWP Bluetooth

以前、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…。

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