Category Archives: ASP.NET Web API

HttpClient を使って同期で通信する

HttpClient はとても使いやすいのですが、async / await の非同期処理のデッドロックにハマることがあります。

デッドロック

次のコードは、WPF におけるデッドロックの例です。

private void Button_Click(object sender, RoutedEventArgs e)
{
    var result = this.GetPersonAsync().Result;
    MessageBox.Show("Hello " + result.Name);
}

private async Task<Person> GetPersonAsync()
{
    using (var client = new HttpClient())
    {
        var response = await client.GetAsync("http://xxx.azurewebsites.net/api/person");
        var responseContent = await response.Content.ReadAsStringAsync();
        if (String.IsNullOrEmpty(responseContent))
        {
            return null;
        }
        return JsonConvert.DeserializeObject<Person>(responseContent);
    }
}

neuecc さんがブログに書かれているように、Result(Wait)と await がお互いに待機していることが原因です。対策としては次のどちらかが施されていれば回避できますが、両方とも行ったほうが良よさそうです。

  1. すべてを async / await で統一して実装する
  2. await しているメソッドに ConfigureAwait(false) を実装する

同期版のメソッドを実装する

別のアプローチとして、同期版の GetPerson メソッドを実装してみました。まず、async / await の非同期メソッドを同期処理で呼び出せるヘルパークラスを作ります。こちらのサイトから、そのままコピーしてきました。Unwrap しているところがポイントです。

internal static class AsyncHelper
{
    private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

    public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        return AsyncHelper._myTaskFactory.StartNew<Task<TResult>>(func).Unwrap<TResult>().GetAwaiter().GetResult();
    }

    public static void RunSync(Func<Task> func)
    {
        AsyncHelper._myTaskFactory.StartNew<Task>(func).Unwrap().GetAwaiter().GetResult();
    }
}

このヘルパークラスを使って同期版の GetPerson メソッドを実装し、ボタンクリックイベントから呼び出します。この方法なら、デッドロックせずに同期で通信することができます。

private void Button_Click(object sender, RoutedEventArgs e)
{
    var result = this.GetPerson();
    MessageBox.Show("Hello " + result.Name);
}

private Person GetPerson()
{
    return AsyncHelper.RunSync<Person>(() => this.GetPersonAsync());
}

まとめ

非同期メソッドをライブラリで提供している場合、同期メソッドも提供すれば、アプリ側で Result(Wait)することもなく、デッドロックを発生させてしまうことが少なくなると思います。デットロックが起きる可能性はゼロではないので、async / await の非同期処理を正しく理解して使いこなしていく必要はあります。

ASP.NET でクライアントの IP アドレスを取得する

ASP.NET MVC や Web API で、クライアントの IP アドレスを取得する方法をまとめておきます。プロキシサーバーなどを経由して Web サーバーに接続された場合、HTTP ヘッダーの X-Forwarded-For から取得する必要があります。

ASP.NET MVC

サーバー環境変数から取得しているため、HTTP_X_FORWARDED_FOR がキーとなります。

public ActionResult Index()
{
    var clientIp = "";
    var xForwardedFor = Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
    if (String.IsNullOrEmpty(xForwardedFor) == false)
    {
        clientIp = xForwardedFor.Split(',').GetValue(0).ToString().Trim();
    }
    else
    {
        clientIp = Request.UserHostAddress;
    }

    if (clientIp != "::1"/*localhost*/)
    {
        clientIp = clientIp.Split(':').GetValue(0).ToString().Trim();
    }

    ViewBag.Message = clientIp;
    return View();
}

ASP.NET Web API

HTTP ヘッダーの X-Forwarded-For から取得します。OWIN を使っている場合は、MS_HttpContext ではなく、MS_OwinContext の RemoteIpAddress から取得します。

public string Get()
{
    IEnumerable<string> headerValues;
    var clientIp = "";
    if (ControllerContext.Request.Headers.TryGetValues("X-Forwarded-For", out headerValues) == true)
    {
        var xForwardedFor = headerValues.FirstOrDefault();
        clientIp = xForwardedFor.Split(',').GetValue(0).ToString().Trim();
    }
    else
    {
        if (ControllerContext.Request.Properties.ContainsKey("MS_HttpContext"))
        {
            clientIp = ((HttpContextWrapper)ControllerContext.Request.Properties["MS_HttpContext"]).Request.UserHostAddress;
        }
    }

    if (clientIp != "::1"/*localhost*/)
    {
        clientIp = clientIp.Split(':').GetValue(0).ToString().Trim();
    }
    return clientIp;
}

WCF

おまけです。WCF の場合は、HttpRequestMessageProperty から HTTP ヘッダーの情報を取得できます。

public string GetData(int value)
{
    var clientIp = "";
    var properties = OperationContext.Current.IncomingMessageProperties;
    var httpRequestMessage = properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
    if (httpRequestMessage.Headers["X-Forwarded-For"] != null)
    {
        var xForwardedFor = httpRequestMessage.Headers["X-Forwarded-For"];
        clientIp = xForwardedFor.Split(',').GetValue(0).ToString().Trim();
    }
    else
    {
        var remoteEndpointMessage = properties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
        clientIp = remoteEndpointMessage.Address;
    }

    if (clientIp != "::1"/*localhost*/)
    {
        clientIp = clientIp.Split(':').GetValue(0).ToString().Trim();
    }
    return clientIp;
}

以上です。

SuppressFormsAuthenticationRedirect プロパティが便利だった件

.NET Framework 4.5 から追加された SuppressFormsAuthenticationRedirect プロパティが便利でした。フォーム認証を設定している ASP.NET MVC アプリと同じプロジェクトに ASP.NET Web API を実装した場合、Web API の Controller のアクションメソッドで HttpStatusCode.Unauthorized(401)を返しても、クライアント側には Location ヘッダーにログインページの URL が設定された HttpStatusCode.Found(302)に書き変えられた結果が返されます。Web API でこの動作だと都合が悪いときには、SuppressFormsAuthenticationRedirect プロパティに true をセットすると、HttpStatusCode.Unauthorized(401)を返すことができます。

前準備

Visual Studio から、ASP.NET Web API のプロジェクトを作成し、MVC アプリケーションにフォーム認証を設定します。

まず、HomeController クラスに Authorize 属性を付加します。

[Authorize]
public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

Web.config でフォーム認証を有効にします。

  <system.web>
    <authentication mode="Forms">
      <forms loginUrl="/Account/Login/" />
    </authentication>
  </system.web>

ログインとログアウトのアクションメソッドをもつ AccountController クラスと ビュー を実装します。

[AllowAnonymous]
public class AccountController : Controller
{
    public ActionResult Login()
    {
        return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Login(string model)
    {
        FormsAuthentication.SetAuthCookie("Guest", false);
        return RedirectToAction("Index", "Home");
    }

    public ActionResult Logout()
    {
        FormsAuthentication.SignOut();
        return RedirectToAction("Login", "Account");
    }
}
@{
    ViewBag.Title = "Login";
}

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <div class="form-group">
        <input type="submit" value="Login" class="btn btn-default" />
    </div>
}

HttpStatusCode.Unauthorized(401)を返す Web API の TestController クラスを実装します。

public class TestController : ApiController
{
    [Route("api/test")]
    public IHttpActionResult Get()
    {
        return StatusCode(HttpStatusCode.Unauthorized);
    }
}

結果確認

プログラムを実行し、IE の F12 開発者ツールでキャプチャしてみます。GET api/test をリクエストすると、HttpStatusCode.Found(302)が返されてログインページにリダイレクトされていることが分かります。

302

SuppressFormsAuthenticationRedirect プロパティに true をセットするように変更し、再度プログラムを実行します。

public class TestController : ApiController
{
    [Route("api/test")]
    public IHttpActionResult Get()
    {
        HttpContext.Current.Response.SuppressFormsAuthenticationRedirect = true;
        return StatusCode(HttpStatusCode.Unauthorized);
    }
}

今度は、HttpStatusCode.Unauthorized(401)が返されるようになりました。

401

まとめ

いろいろと調べていると、FormsAuthenticationModule に書き換えられたステータスをカスタムモジュールで再度書き換える方法もありました。

.NET Framework 4.5 以降であれば、SuppressFormsAuthenticationRedirect プロパティを使うのが便利そうです。

Semantic Logging Application Block で ETW ログを Azure Search に出力する

先月、Build Insider MEETUP with Grani に参加してきました。C# で Web アプリケーションを作るフレームワークの話のなかで、Semantic Logging Application Block(SLAB)が面白そうだったので、試してみました。

SLAB は、Event Tracing for Windows(ETW)のログを出力できるライブラリであり、Enterprise Library 6.0 に含まれるアプリケーションブロックのひとつです。厳密に型付けされたイベントをベースにしたログは、コンソールアプリ、テキストファイル、SQL Server、Azure Table Storage、Elasticsearch などに出力できます。Elasticsearch に出力できるなら、Azure Search に出力してもよさそうなので試してみました。ASP.NET Web API で発生した例外をハンドリングして、ETW ログを出力するシナリオです。

ETW ログを書き込む

ASP.NET Web API の Web アプリケーションを作成し、ETW ログを書き込むクラスを作成します。EventSource クラスを継承したクラスを作成し、ログを書き込む Error メソッドを実装します。ログに書き込む情報は、HTTP メソッド、URL の絶対パス、例外メッセージの3つとします。

[EventSource(Name = "MyEventSource")]
public sealed class MyEventSource : EventSource
{
    private readonly static MyEventSource log = new MyEventSource();
    private MyEventSource() { }
    public static MyEventSource Log { get { return log; } }

    [Event(1, Level = EventLevel.Error, Message = "An error has occurred")]
    public void Error(string method, string path, string exceptionMessage)
    {
        WriteEvent(1, method, path, exceptionMessage);
    }
}

例外をハンドリングする ExceptionLogger クラスを継承したクラスを作成してロギングします。

public class MyExceptionLogger : ExceptionLogger
{
    public override void Log(ExceptionLoggerContext context)
    {
        var method = context.Request.Method.ToString();
        var path = context.Request.RequestUri.AbsolutePath;
        var exceptionMessage = context.Exception.Message;

        MyEventSource.Log.Error(method ?? "", path ?? "", exceptionMessage ?? "");
    }
}

MyExceptionLogger クラスは、WebApiConfig クラスで有効化しておきます。

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Services.Add(typeof(IExceptionLogger), new MyExceptionLogger());

        (省略)
        );
    }
}

Azure Search のアクセスクラスを作成する

Web アプリケーションのプロジェクトに、Azure Search Library(Preview)を NuGet からインストールします。ログのデータをモデルに詰め替えたいので、AutoMapper も合わせてインストールします。

  • Install-Package Microsoft.Azure.Search -Pre
  • Install-Package AutoMapper -Version 3.3.1

Azure Search に対して、インデックスの作成、ログのアップロード、ログの検索を行うアクセスクラスを作成します。

public class EventSearchClient
{
    private readonly string _serviceName;
    private readonly string _apiKey;
    private readonly string _indexName;

    public EventSearchClient(string serviceName, string apiKey)
    {
        this._serviceName = serviceName;
        this._apiKey = apiKey;
        this._indexName = "event";
    }

    public async Task CreateIndexAsync()
    {
        using (var serviceClient = new SearchServiceClient(this._serviceName, new SearchCredentials(this._apiKey)))
        {
            // インデックスの削除
            if (await serviceClient.Indexes.ExistsAsync(this._indexName))
            {
                await serviceClient.Indexes.DeleteAsync(this._indexName);
            }

            // インデックスの作成
            var definition = new Index()
            {
                Name = this._indexName,
                Fields = new[]
                    {
                        new Field("id", DataType.String)    { IsKey = true },
                        new Field("providerId", DataType.String)  { IsSearchable = true, IsFilterable = true },
                        new Field("eventId", DataType.Int32)  { IsFilterable = true },
                        new Field("keywords", DataType.Int32)  { IsFilterable = true },
                        new Field("level", DataType.Int32)  { IsFilterable = true },
                        new Field("message", DataType.String)  { IsSearchable = true, IsFilterable = true },
                        new Field("opcode", DataType.Int32)  { IsFilterable = true },
                        new Field("task", DataType.Int32)  { IsFilterable = true },
                        new Field("version", DataType.Int32)  { IsFilterable = true },
                        new Field("eventName", DataType.String)  { IsSearchable = true, IsFilterable = true },
                        new Field("timestamp", DataType.DateTimeOffset)  { IsFilterable = true, IsSortable = true },
                        new Field("processId", DataType.Int32)  { IsFilterable = true },
                        new Field("threadId", DataType.Int32)  { IsFilterable = true },
                        new Field("method", DataType.String)  { IsSearchable = true, IsFilterable = true },
                        new Field("path", DataType.String)  { IsSearchable = true, IsFilterable = true },
                        new Field("exceptionMessage", DataType.String)  { IsSearchable = true, IsFilterable = true },
                    }
            };
            await serviceClient.Indexes.CreateAsync(definition);
        }
    }

    public async Task UploadAsync(string json)
    {
        using (var serviceClient = new SearchServiceClient(this._serviceName, new SearchCredentials(this._apiKey)))
        using (var indexClient = serviceClient.Indexes.GetClient(this._indexName))
        {
            // JSON 形式のログを、Azure Search で検索しやすい MyEventEntryModel 型に変換
            var eventEntry = JsonConvert.DeserializeObject<List<MyEventEntry>>(json);
            var documents = eventEntry.Select(x => Mapper.Map<MyEventEntryModel>(x));
            try
            {
                // ログのアップロード
                await indexClient.Documents.IndexAsync(IndexBatch.Create(documents.Select(x => IndexAction.Create(x))));
            }
            catch (IndexBatchException e)
            {
                var message = String.Format(
                    "Failed to index some of the documents: {0}",
                    String.Join(", ", e.IndexResponse.Results.Where(r => !r.Succeeded).Select(r => r.Key)));
                throw new Exception(message);
            }
            await Task.Delay(2000); // 非同期で作成されるので、少し待機する
        }
    }

    public async Task<IEnumerable<MyEventEntryModel>> SearchAsync(string searchText)
    {
        using (var serviceClient = new SearchServiceClient(this._serviceName, new SearchCredentials(this._apiKey)))
        using (var indexClient = serviceClient.Indexes.GetClient(this._indexName))
        {
            var sp = new SearchParameters();
            sp.OrderBy = new List<string> { "timestamp desc" };
            var response = await indexClient.Documents.SearchAsync<MyEventEntryModel>(searchText, sp);
            return response.Results.Select(x => x.Document);
        }
    }
}

UploadAsync メソッドでは、JSON 形式で渡されてきたログを MyEventEntry クラスにデシリアライズしています。MyEventEntry クラスは、Payload プロパティに HTTP メソッド、URL の絶対パス、例外メッセージが内包されていて Azure Search では検索し難いため、AutoMapper でフラットな MyEventEntryModel クラスに詰め替えています。

public class MyEventEntry
{
    public string ProviderId { get; set; }
    public int EventId { get; set; }
    public int Keywords { get; set; }
    public int Level { get; set; }
    public string Message { get; set; }
    public int Opcode { get; set; }
    public int Task { get; set; }
    public int Version { get; set; }
    public Payload Payload { get; set; }
    public string EventName { get; set; }
    public DateTime Timestamp { get; set; }
    public int ProcessId { get; set; }
    public int ThreadId { get; set; }
}

public class Payload
{
    public string method { get; set; }
    public string path { get; set; }
    public string exceptionMessage { get; set; }
}
[SerializePropertyNamesAsCamelCase]
public class MyEventEntryModel
{
    public string Id { get; set; }
    public string ProviderId { get; set; }
    public int? EventId { get; set; }
    public int? Keywords { get; set; }
    public int? Level { get; set; }
    public string Message { get; set; }
    public int? Opcode { get; set; }
    public int? Task { get; set; }
    public int? Version { get; set; }
    public string EventName { get; set; }
    public DateTimeOffset? Timestamp { get; set; }
    public int? ProcessId { get; set; }
    public int? ThreadId { get; set; }
    public string Method { get; set; }
    public string Path { get; set; }
    public string ExceptionMessage { get; set; }
}

MyEventEntryModel クラスには、ポイントが3つあります。

  1. SerializePropertyNamesAsCamelCase 属性により、大文字小文字の違いを意識しないで大丈夫
  2. 値型は NULL を許容する必要がある
  3. Azure Search の日付型は、DateTimeOffset

AutoMapper のマッピング定義は、Global.asax の Application_Start メソッドで設定しておきます。

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        (省略)

        Mapper.CreateMap<MyEventEntry, MyEventEntryModel>()
            .ForMember(dst => dst.Method, opt => opt.MapFrom(src => src.Payload.method))
            .ForMember(dst => dst.Path, opt => opt.MapFrom(src => src.Payload.path))
            .ForMember(dst => dst.ExceptionMessage, opt => opt.MapFrom(src => src.Payload.exceptionMessage))
            .ForMember(dst => dst.Timestamp, opt => opt.MapFrom(src => new DateTimeOffset(src.Timestamp)))
            .ForMember(dst => dst.Id, opt => opt.MapFrom(src => String.Format("{0:D19}", DateTime.MaxValue.Ticks - src.Timestamp.Ticks)));
    }
}

SLAB の Custom Event Sink を作成する

SLAB で任意のストレージにログを出力したい場合には、Custom Event Sink を作成します。IObserver<EventEntry> クラスを継承したクラスを作成し、OnNext メソッドでログの出力を実装します。EventSearchClient クラスの UploadAsync メソッドの呼び出しは、UI スレッドに戻さないように ConfigureAwait(false) で待機する必要があります。

public class AzureSearchSink : IObserver<EventEntry>
{
    private readonly string _serviceName;
    private readonly string _apiKey;

    public AzureSearchSink(string serviceName, string apiKey)
    {
        this._serviceName = serviceName;
        this._apiKey = apiKey;
    }

    public void OnCompleted()
    {
    }

    public void OnError(Exception error)
    {
    }

    public void OnNext(EventEntry value)
    {
        if (value != null)
        {
            using (var writer = new StringWriter())
            {
                new JsonEventTextFormatter().WriteEvent(value, writer);
                Post(writer.ToString());
            }
        }
    }

    private void Post(string body)
    {
        var json = String.Format("[{0}]", body);    // JSON配列
        new EventSearchClient(this._serviceName, this._apiKey).UploadAsync(json).ConfigureAwait(false);
    }
}

ObservableEventListener クラスに AzureSearchSink クラスを設定する拡張メソッドを定義し、Global.asax の Application_Start メソッドで設定します。LogToAzureSearch メソッドのサービス名と API キーは、Azure 管理ポータルから取得した情報を設定してください。

public static class AzureSearchSinkExtensions
{
    public static EventListener CreateListener(string serviceName, string apiKey)
    {
        var listener = new ObservableEventListener();
        listener.LogToAzureSearch(serviceName, apiKey);
        return listener;
    }

    public static SinkSubscription<AzureSearchSink> LogToAzureSearch(this IObservable<EventEntry> eventStream, string serviceName, string apiKey)
    {
        var sink = new AzureSearchSink(serviceName, apiKey);
        var subscription = eventStream.Subscribe(sink);
        return new SinkSubscription<AzureSearchSink>(subscription, sink);
    }
}
public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        (省略)

        var listener = new ObservableEventListener();
        listener.EnableEvents(MyEventSource.Log, EventLevel.Error);
        listener.LogToAzureSearch("xxx", "xxx");
    }
}

Web API を作成する

ASP.NET Web API の EventController クラスを作成し、POST メソッドでインデックスの作成、GET メソッドでログの検索を行います。

[RoutePrefix("api/event")]
public class EventController : ApiController
{
    private readonly string _serviceName = "xxx";
    private readonly string _apiKey = "xxx";

    [Route]
    public async Task<IEnumerable<MyEventEntryModel>> Get()
    {
        return await new EventSearchClient(this._serviceName, this._apiKey).SearchAsync("");
    }

    [Route("{searchText}")]
    public async Task<IEnumerable<MyEventEntryModel>> Get(string searchText)
    {
        return await new EventSearchClient(this._serviceName, this._apiKey).SearchAsync(searchText);
    }

    [Route]
    public async Task Post()
    {
        await new EventSearchClient(this._serviceName, this._apiKey).CreateIndexAsync();
    }
}

もうひとつ、例外を発生させる TestController クラスも作成します。

[RoutePrefix("api/test")]
public class TestController : ApiController
{
    [Route("{id:int}")]
    public string Get(int id)
    {
        throw new Exception(String.Format("test{0}", id));
        return "success";
    }
}

結果確認

Google Chrome の Postman を使って、Web API を実行します。

  1. POST api/event/
  2. GET api/test/1/
  3. GET api/test/2/
  4. GET api/test/3/
  5. GET api/event/

上記のような 5 つの Web API を実行すると、Azure Search にインデックスが作成され、発生した例外のログがアップロードされます。結果として、3 件のログを取得できます。

slab01

また、GET api/event/test2/ を実行すると、条件を絞り込んでログを取得することも可能です。

slab02

まとめ

アプリケーションのログは、単に溜め込んでいくだけでは十分な分析や活用ができないので、活用しやすい構造で保管していくことが重要です。SLAB と ETW を使えば、セマンティックな構造化されたログを、任意のストレージに出力することができます。コンソールや Windows Service を使って SLAB を Out of Process でホストさせることも可能です。自前でログを作り込むのもよいですが、SLAB はなかなか面白いライブラリなので、使ってみるのもありだと思いました。

ASP.NET Web API で Jil JSON Serializer を使ってみる

この記事は、ASP.NET Advent Calendar 2014 の2日目の記事です。

「ASP.NET Web API の パフォーマンスを改善するための8つの方法」というブログから、JSON のシリアライズを高速化する方法として、MediaTypeFormatter を Jil JSON Serializer  に置き換える方法を紹介します。

Jil JSON Serializerとは

Jil JSON Serializer は、高速に JSON をシリアライズできるライブラリです。ASP.NET Web API の JSON シリアライズは、デフォルトで JSON.NET が使われており、事実上 .NET の標準 JSON Serializer と言えるライブラリですが、GitHub にあるベンチマーク結果を見ると、JSON.NET などのライブラリと比較して高速であることが分かります。使いかたはシンプルで、NuGet からインストールし、Jil.JSON クラスの Serialize() と Deserialize() を呼び出すだけです。

MediaTypeFormatter を置き換える

Visual Studio 2013 で、WebApplication のプロジェクトを選択し、Web API のテンプレートで作成します。Jil JSON Serializer の C# 用ライブラリの NuGet パッケージをインストールします。

  • Install-Package Jil

Jil JSON Serializer を使った MediaTypeFormatter を作成します。

public class JilFormatter : MediaTypeFormatter
{
    private readonly Options _jilOptions;

    public JilFormatter()
    {
        _jilOptions = new Options(dateFormat: DateTimeFormat.ISO8601);
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));

        SupportedEncodings.Add(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true));
        SupportedEncodings.Add(new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true));
    }

    public override bool CanReadType(Type type)
    {
        if (type == null)
        {
            throw new ArgumentNullException("type");
        }
        return true;
    }

    public override bool CanWriteType(Type type)
    {
        if (type == null)
        {
            throw new ArgumentNullException("type");
        }
        return true;
    }

    public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
    {
        using (var reader = new StreamReader(readStream))
        {
            return Task.FromResult(JSON.Deserialize(reader, type, _jilOptions));
        }
    }

    public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
    {
        using (TextWriter streamWriter = new StreamWriter(writeStream))
        {
            JSON.Serialize(value, streamWriter, _jilOptions);
            return Task.FromResult(writeStream);
        }
    }
}

デフォルトの JSON Serializer を削除して、JilFormatter に置き換えます。

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Formatters.RemoveAt(0);
        config.Formatters.Insert(0, new JilFormatter());

        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

まとめ

ASP.NET Web API では、今回の MediaTypeFormatter の置き換えのように、Framework 側で提供される機能のカスタマイズが簡単にできます。開発者は、Microsoft が提供するライブラリだけでなく、自分の好きなライブラリを自由に組み合わせて、Application を作ることができます。この One ASP.NET の思想が ASP.NET 5(旧 ASP.NET vNext )にも引き継がれて、さらに進化して使いやすくなっていくことを期待しています。