ASP.NET MVC(及びASP.NET MVC Core)にて、JsonResultを返すコントローラアクションがあったとします。
この時、返されるオブジェクトのプロパティにEnumがある場合、数値ではなく列挙型名でシリアル化する方法をまとめます。
ResultType.OK という列挙型が、"1" ではなく "OK"という文字でシリアル化できるようになります。
public class ResultViewModel
{
public ResultType Result { get; set; }
}
public enum ResultType
{
NG,
OK
}
{"Result":0}
{"Result":"NG"}
個別のまとめはよく見るのですが、ASP.NET MVCでの方法と、ASP.NET MVC Coreでの方法が混在している為、ASP.NET MVCアプリケーションをCoreに置き換える時などの参考用に両方のやり方をまとめておきます。
またその過程でそのまま使用できるアクションフィルタやJsonResult派生クラスなども提供します。
いいねたくさん頂けましたら、動作するサンプルをGitHubで共有したいと思います。
やりたいこと
- JsonResultが生成する列挙型の値を数値ではなく列挙型名でシリアル化する。
- システム全体のグローバル設定とし、コントローラ側で意識しなくて良いようにする。
この記事でわかること
- ASP.NET MVC Core(.NET Core/5+)でSystem.Text.Jsonを使って列挙型を文字列にする。
- ASP.NET MVC(.NET Framework)でNewtonsoft.Jsonを使って列挙型を文字列にする。また、それをグローバルに設定する。
- ASP.NET MVC(.NET Framework)でSystem.Text.Jsonを使って列挙型を文字列にする。また、それをグローバルに設定する。
- 2, 3をC#とVB.NETで行う。
ASP.NET MVC 5 Core での方法
.NET 5では、JSONシリアライザーとして標準のSystem.Text.Jsonが用意されており、ASP.NET MVC 5 Coreでも規定としてSystem.Text.Jsonが使われます。
列挙型の値を列挙型名(文字列)で返すには、その列挙型の定義か、列挙型を使用しているプロパティにJsonConverter属性を付与するだけです。
using System.Text.Json.Serialization;
型定義に指定する方法
public class ResultViewModel
{
public ResultType Result { get; set; }
public string ErrorMessage { get; set; } = "Hello!";
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ResultType
{
NG,
OK
}
プロパティ側に指定する方法
public class ResultViewModel
{
[JsonConverter(typeof(JsonStringEnumConverter))]
public ResultType Result { get; set; }
public string ErrorMessage { get; set; } = "Hello!";
}
public enum ResultType
{
NG,
OK
}
共通
public IActionResult GetData()
{
var vm = new TestViewModel() { Result = ResultType.NG };
return Json(vm);
}
{"result":"NG","errorMessage":"Hello!"}
列挙型に限りませんが、ASP.NET MVC Coreで使用されるSystem.Text.Jsonは、標準でプロパティ名を小文字から始まるキャメルケース(ロワーキャメルケース)で置き換えます。
その為、上記のResultプロパティは"result"のように先頭が小文字のプロパティ名に置き換えられます。
ErrorMessageプロパティも、"errorMessage"のように先頭が小文字のプロパティ名に置き換えられます。
これが困る場合、システム全体の設定として、キャメルケースをオフにすることもできます。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews().AddJsonOptions(opts =>
{
// PropertyNamingPolicyをnullに設定するとプロパティ名の変換が行われない。
opts.JsonSerializerOptions.PropertyNamingPolicy = null; // default: System.Text.Json.JsonNamingPolicy.CamelCase;
});
}
もし、システム全体の設定として一律に列挙型の値を列挙型名にしたい場合は次のようにすることもできます。
が、あまり使うことはないでしょう。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews().AddJsonOptions(opts =>
{
// PropertyNamingPolicyをnullに設定するとプロパティ名の変換が行われない。
opts.JsonSerializerOptions.PropertyNamingPolicy = null; // default: System.Text.Json.JsonNamingPolicy.CamelCase;
// 列挙型の値を列挙型名に変換する。
opts.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
});
}
{"Result":"NG","ErrorMessage":"Hello!"}
同じく列挙型に限りませんが、個々のプロパティ名をJsonPropertyName属性で指定することもできます。
システム全体の設定で指定したPropertyNamingPolicyと被った場合、個々の設定が優先されます。
public class ResultViewModel
{
[JsonPropertyName("Result")]
public ResultType Result { get; set; }
[JsonPropertyName("ErrMsg")]
public string ErrorMessage { get; set; } = "Hello!";
}
{"Result":"NG","ErrMsg":"Hello!"}
ASP.NET MVC 5 での方法
ASP.NET MVC 5では、System.Web.Script.Serialization.JavaScriptSerializerがシリアライズを担当しますが、その呼び出しはJsonResult自身が内部で直接行っています。
その為、その挙動を利用者が変更するにはJsonResultを置き換えるか、JsonResultを使わずに直接JSON文字列を返すしかありません。
ASP.NET MVCで一般的に利用されているJSONシリアライザは、JSON.NETと呼ばれるNewtonsoft.Jsonパッケージです。NuGetからインストールできます。
まずコントローラ側で対応するプリミティブなやり方を紹介し、次にそれをシステム全体に適用するやり方を紹介します。
その後、新しいJSONシリアライザであるSystem.Text.Jsonを使う方法も紹介します。
基本的には、システム全体に適用するやり方でいくことになると思います。
順を追って説明していきます。
コントローラ側で対応する方法(指定したモデルクラスのみ変換)
モデルクラスや列挙型の定義にNewtonsoft.Json.JsonConverter属性を指定する方法です。
ASP.NET MVCはNewtonsoft.Jsonを知らないので、この属性を指定するだけでは何も起こりません。
そこで、コントローラアクションにてJsonメソッドではなくContentメソッドを使ってNewtonsoft.Jsonで変換した結果の文字列を直接返すようにします。コントローラアクションの戻り値はJsonResultではなくActionResultにする必要があります。
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
public ActionResult GetData()
{
var vm = new TestViewModel() { Result = ResultType.NG };
// 変換結果をContentとして返す(コントローラアクションの戻り値はJsonResultではなくActionResult)
return Content(JsonConvert.SerializeObject(vm), "application/json");
}
public class ResultViewModel
{
public ResultType Result { get; set; }
public string ErrorMessage { get; set; } = "Hello!";
}
[Newtonsoft.Json.JsonConverter(typeof(StringEnumConverter))]
public enum ResultType
{
NG,
OK
}
{"Result":"NG","ErrorMessage":"Hello!"}
Resultプロパティに直接JsonConverter属性を指定するやり方でも可能です。
public class ResultViewModel
{
[Newtonsoft.Json.JsonConverter(typeof(StringEnumConverter))]
public ResultType Result { get; set; }
public string ErrorMessage { get; set; } = "Hello!";
}
public enum ResultType
{
NG,
OK
}
コントローラ側で対応する方法(全てのモデルクラスを変換)
モデルクラスに個々に属性を指定するのではなく、全てのモデルクラスの列挙型を文字列に変換する方法です。
あまりこういうことはやらないと思いますが、プロパティ名の変換ルールをシステム全体で統一したい時など、属性指定以外にもこういうプリミティブな方法があると知っておくと便利です。
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
public ActionResult GetData()
{
var vm = new TestViewModel() { Result = ResultType.NG };
// JSON.NET設定クラス
var settings = new Newtonsoft.Json.JsonSerializerSettings();
// 列挙型を文字列にするコンバータの生成
var converter = new StringEnumConverter() {
NamingStrategy = new DefaultNamingStrategy() // 列挙型名をそのまま値とする
};
settings.Converters.Add(converter);
// 変換結果をContentとして返す(コントローラアクションの戻り値はJsonResultではなくActionResult)
return Content(JsonConvert.SerializeObject(vm, settings), "application/json");
}
public class ResultViewModel
{
public ResultType Result { get; set; }
public string ErrorMessage { get; set; } = "Hello!";
}
public enum ResultType
{
NG,
OK
}
{"Result":"NG","ErrorMessage":"Hello!"}
注意事項として、StringEnumConverterの生成時にNamingStrategyを指定しないと、変換が行われず数値が出力されてしまいます。規定値はnullとなっています。
コントローラ側で対応する方法(全てのモデルクラスを変換)【グローバル設定を利用】
前述の方法で、設定のみをGlobal.asaxに記述する方法です。
Newtonsoft.Json.JsonConvert.DefaultSettings に共通の設定を保存します。
こうすることで、システム全体で設定を共有できます。
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
... 略 ...
Newtonsoft.Json.JsonConvert.DefaultSettings = () =>
{
// JSON.NET設定クラス
var settings = new Newtonsoft.Json.JsonSerializerSettings();
// 列挙型を文字列にするコンバータの生成
var converter = new StringEnumConverter() {
NamingStrategy = new DefaultNamingStrategy() // 列挙型名をそのまま値とする
};
settings.Converters.Add(converter);
return settings;
};
}
public ActionResult GetData()
{
var vm = new TestViewModel() { Result = ResultType.NG };
// 変換結果をContentとして返す(コントローラアクションの戻り値はJsonResultではなくActionResult)
return Content(JsonConvert.SerializeObject(vm), "application/json");
}
public class ResultViewModel
{
public ResultType Result { get; set; }
public string ErrorMessage { get; set; } = "Hello!";
}
public enum ResultType
{
NG,
OK
}
システム全体でNewtonsoft.Jsonを利用するする方法
この方法がこの記事の本命ですが、少々複雑です。
これまでのやり方だと、コントローラー側で個々にJsonConvert.SerializeObjectを明示的に呼び出す必要がありました。戻り値もJsonResultでなくなっています。
そうではなく、コントローラ側は標準的なJsonメソッドを利用してJsonResultを返すだけで、裏側で自動的にNewtonsoft.Jsonを使ってJSONシリアライズしてくれると便利です。
ここでは、そのやり方を紹介します。
こちらで紹介されていたやり方になります。
https://stackoverflow.com/questions/7109967/using-json-net-as-the-default-json-serializer-in-asp-net-mvc-3-is-it-possible
手順としては、JsonResultをこっそり置き換える為のJsonNetResultというクラスを、JsonResultを継承して作り、その内部でNewtonsoft.Jsonを使ってシリアライズするようにします。
そして、カスタムアクションフィルタを使ってOnActionExecuted(アクション実行後フィルタ)でfilterContent.ResultをJsonResultからJsonNetResultに置き換えます。
まず、JsonNetResultの説明です。
public class JsonNetResult : JsonResult
{
public JsonNetResult()
{
Settings = null;
}
public JsonSerializerSettings Settings { get; private set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("JSON GET is not allowed");
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = string.IsNullOrEmpty(this.ContentType) ? "application/json" : this.ContentType;
if (this.ContentEncoding != null)
response.ContentEncoding = this.ContentEncoding;
if (this.Data == null)
return;
var scriptSerializer = this.Settings == null
? JsonSerializer.CreateDefault()
: JsonSerializer.Create(this.Settings);
scriptSerializer.Serialize(response.Output, this.Data);
}
}
上記の内容はどういうものかというと、元となるJsonResultのExecuteResultの実装を(オープンソースなのでGitHubから)そのまま持ってきて、JavascriptSerializerを使っている箇所をNewtonsoft.JsonのJsonSerializerに置き換えているだけです。
次に、このJsonNetResultを使うようにするカスタムアクションフィルタ、JsonHandlerAttributeを作ります。
これはコントローラアクションの後処理フィルタで、JsonResultをJsonNetResultに置き換えます。
コントローラの戻り値がJsonResult以外の場合には何も行いません。
public class JsonHandlerAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var jsonResult = filterContext.Result as JsonResult;
if (jsonResult != null)
{
filterContext.Result = new JsonNetResult
{
ContentEncoding = jsonResult.ContentEncoding,
ContentType = jsonResult.ContentType,
Data = jsonResult.Data,
JsonRequestBehavior = jsonResult.JsonRequestBehavior
};
}
base.OnActionExecuted(filterContext);
}
}
そして、FilterConfigにて、JsonHandlerAttributeフィルタをグローバルフィルタとして登録します。
こうすることで、全てのコントローラアクションの後処理としてこのJsonHandlerAttributeが呼ばれるようになります。
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
... 略 ...
filters.Add(new JsonHandlerAttribute());
}
}
コントローラアクションの方は、以下のように普通にJsonResultを返します。普通のASP.NET MVCのJSONを返すアクションの書き方です。
そして、変換したいプロパティや列挙型に、通常通りJsonConverter属性を指定します。
public JsonResult GetData()
{
var vm = new TestViewModel() { Result = ResultType.NG };
// JsonResultで返す
return Json(vm, JsonRequestBehavior.AllowGet);
}
public class ResultViewModel
{
public ResultType Result { get; set; }
public string ErrorMessage { get; set; } = "Hello!";
}
[Newtonsoft.Json.JsonConverter(typeof(StringEnumConverter))]
public enum ResultType
{
NG,
OK
}
{"Result":"NG","ErrorMessage":"Hello!"}
グローバルフィルタは使わず個別にJsonHandlerAttributeを使う
FilterConfigに指定せず、以下のように個別にコントローラアクションに属性を指定することもできます。
システム全体への影響を抑えながら局所的に対応したい場合はこちらの方が便利でしょう。
[JsonHandler]
public JsonResult GetData()
{
var vm = new TestViewModel() { Result = ResultType.NG };
// JsonResultで返す
return Json(vm, JsonRequestBehavior.AllowGet);
}
EnumMemberAttributeを使う
Newtonsoft.Jsonでは、EnumMemberAttributeを使って列挙型の値に自由に文字列を指定することができます。
あまり使う事はないかもしれませんが、場合によっては便利かもしれません。
[Newtonsoft.Json.JsonConverter(typeof(StringEnumConverter))]
public enum ErrorMessageID
{
[EnumMember(Value:="E001")]
E001_TooLongString,
[EnumMember(Value:="E002")]
E002_InputRequired,
:
}
public class ResultViewModel
{
public ErrorMessageID ErrorMessageID { get; set; }
}
{"ErrorMessageID":"E001"}
ASP.NET MVC 5でもSystem.Text.Jsonを使う方法
「将来的にASP.NETからASP.NET Coreへ移行する予定だが、現行のASP.NETシステムに手をいれなければならず、将来の互換性確保の為にNewtonsoft.JsonよりもSystem.Text.Jsonを使いたい」という人もいるかもしれません。
その場合は、NuGetからSystem.Text.Jsonをインストールして使用することができます。
System.Text.Jsonでは、現在のところまだ先ほどのEnumMemberAttributeに相当するものがサポートされていませんが、拡張ライブラリが存在しており、同様にNuGetから取得できます。
https://github.com/Macross-Software/core/tree/develop/ClassLibraries/Macross.Json.Extensions#enumerations
SystemTextJsonResultを作る
Newtonsoft.JsonではなくSystem.Text.Jsonを使うには、先ほどのJsonNetResultを次のように置き換えたSystemTextJsonResultを作ります。
using System.Text.Json;
using System.Text.Json.Serialization;
public class SystemTextJsonResult : JsonResult
{
public SystemTextJsonResult()
{
Settings = null;
}
public JsonSerializerOptions Settings { get; private set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("JSON GET is not allowed");
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = string.IsNullOrEmpty(this.ContentType) ? "application/json" : this.ContentType;
if (this.ContentEncoding != null)
response.ContentEncoding = this.ContentEncoding;
if (this.Data == null)
return;
var stringValue = (this.Settings == null)
? JsonSerializer.Serialize(this.Data)
: JsonSerializer.Serialize(this.Data, this.Settings);
response.Output.Write(stringValue);
}
}
JsonHandlerAttributeは次のように、JsonNetResultではなくSytemTextJsonResultを使うように変更します。
public class JsonHandlerAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var jsonResult = filterContext.Result as JsonResult;
if (jsonResult != null)
{
filterContext.Result = new JsonNetResult
{
ContentEncoding = jsonResult.ContentEncoding,
ContentType = jsonResult.ContentType,
Data = jsonResult.Data,
JsonRequestBehavior = jsonResult.JsonRequestBehavior
};
}
base.OnActionExecuted(filterContext);
}
}
FilterConfigで、同様にJsonHandlerAttributeをグローバルフィルタとして登録します。
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
... 略 ...
filters.Add(new JsonHandlerAttribute());
}
}
列挙型の定義には、System.Text.Json.Serialization.JsonConverter属性を使ってJsonStringEnumConverterを指定します。
public JsonResult GetData()
{
var vm = new TestViewModel() { Result = ResultType.NG };
// JsonResultで返す
return Json(vm, JsonRequestBehavior.AllowGet);
}
public class ResultViewModel
{
public ResultType Result { get; set; }
public string ErrorMessage { get; set; } = "Hello!";
}
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]
public enum ResultType
{
NG,
OK
}
{"Result":"NG","ErrorMessage":"Hello!"}
SystemTextJsonResult でシリアライズの設定をする
System.Text.Jsonのシリアライズ設定を行うには、JsonSerializerOptionsを生成してJsonSerializer.Serializeの第二引数に指定します。2021年8月24日現在の所、グローバルな設定を行う方法は用意されていないようです(あったら教えて下さい。将来的に用意される方向のようです)。
ですので、シリアライズ設定は、SystemTextJsonResultの中で行うのがとりあえず手っ取り早いでしょう。
using System.Text.Json;
using System.Text.Json.Serialization;
public class SystemTextJsonResult : JsonResult
{
public SystemTextJsonResult()
{
Settings = new JsonSerializerOptions();
var stringEnumConverter = new Serialization.JsonStringEnumConverter();
Settings.Converters.Add(stringEnumConverter);
}
public JsonSerializerOptions Settings { get; private set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("JSON GET is not allowed");
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = string.IsNullOrEmpty(this.ContentType) ? "application/json" : this.ContentType;
if (this.ContentEncoding != null)
response.ContentEncoding = this.ContentEncoding;
if (this.Data == null)
return;
var stringValue = this.Settings == null
? JsonSerializer.Serialize(this.Data)
: JsonSerializer.Serialize(this.Data, this.Settings);
response.Output.Write(stringValue);
}
}
主な変更点を以下に抜粋します。
Settingsプロパティを追加し、コンストラクタで設定を生成してプロパティに保存しています。
この設定を行うと、全ての列挙型を一律に文字列に変換します(モデル側に属性の指定は要りません)。
public SystemTextJsonResult()
{
Settings = new JsonSerializerOptions();
var stringEnumConverter = new Serialization.JsonStringEnumConverter();
Settings.Converters.Add(stringEnumConverter);
}
以下のようにすると、プロパティ名をロワーキャメルケースにします。
public SystemTextJsonResult()
{
Settings = new JsonSerializerOptions(JsonSerializerDefault.General) {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
上記ではJsonSerializerOptionsのコンストラクタ引数にJsonSerializerDefault.Generalを指定していますが、これは今のところ(2021年8月20日現在)何もしないオプションです。
ここにJsonSerializerDefault.Webを指定すると、Web用のおすすめ設定を生成してくれます。
どんなお勧め設定か見る為に、JsonSerializerOptionsのソースコードから引用します。
public JsonSerializerOptions(JsonSerializerDefaults defaults) : this()
{
if (defaults == JsonSerializerDefaults.Web)
{
_propertyNameCaseInsensitive = true;
_jsonPropertyNamingPolicy = JsonNamingPolicy.CamelCase;
_numberHandling = JsonNumberHandling.AllowReadingFromString;
}
else if (defaults != JsonSerializerDefaults.General)
{
throw new ArgumentOutOfRangeException(nameof(defaults));
}
}
以下をセットでやってくれています。
- CamelCaseの指定
- デシリアライズ時に大文字小文字を区別しない指定(CamelCaseに変換してしまうので当然ですね)
- 数値項目を文字列でも取得可能にする(デフォルトではやってくれない!)指定。
JsonSerializerDefault.Webを使うには、以下のようにJsonSerializerOptionsのコンストラクタ引数に指定します。
public SystemTextJsonResult()
{
Settings = new JsonSerializerOptions(JsonSerializerDefault.Web);
}
もし、NumberHandling(数値項目のシリアライズ方法)を規定以外にしたかったら、次のように書けます。
public SystemTextJsonResult()
{
Settings = new JsonSerializerOptions(JsonSerializerDefault.Web)
{
NumberHandling =
JsonNumberHandling.AllowReadingFromString |
JsonNumberHandling.WriteAsString
};
}
SystemTextJsonResultが出来たら、あとはそれをJsonHandlerAttributeから呼び出すように修正するだけです。
変わっている個所は、JsonNetResultをSystemTextJsonResultに置き換えただけです。
public class JsonHandlerAttribute : ActionFilterAttribute
{
public JsonHandlerAttribute(
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var jsonResult = filterContext.Result as JsonResult;
if (jsonResult != null)
{
filterContext.Result = new SystemTextJsonResult()
{
ContentEncoding = jsonResult.ContentEncoding,
ContentType = jsonResult.ContentType,
Data = jsonResult.Data,
JsonRequestBehavior = jsonResult.JsonRequestBehavior
};
}
base.OnActionExecuted(filterContext);
}
}
SystemTextJsonResultのシリアライズ設定を外部に出す
ただ、実際のところ、そのシステムの規定のJSONシリアライズ設定がSystemTextJsonResultのようなライブラリクラスの中に記述されるのは少々問題がありますよね。できればそのような設定は外部から指定できるようになっていると嬉しいです。
そこで、JsonHandlerAttributeのコンストラクタからJsonResultのファクトリ(インスタンス生成関数)を受け取って使うようにします。そうすることでグローバルな設定をFilterConfigで記述することができるようになります。また、JsonHandlerAttribute自身がJsonResult派生の具象クラスに依存することもなくなります。
属性指定の場合でも使えるよう、スタティックプロパティとしてグローバルなデフォルトファクトリも指定できるようにしましょう。
Func<JsonResult> JsonResultFactory
と、static Func<JsonResult> DefaultJsonResultFactory
を追加します。
public class JsonHandlerAttribute : ActionFilterAttribute
{
public Func<JsonResult> JsonResultFactory { get; set; } = null;
public static Func<JsonResult> DefaultJsonResultFactory { get; set; } = null;
public JsonHandlerAttribute() : base()
{
}
public JsonHandlerAttribute(Func<JsonResult> jsonResultFactory) : this()
{
JsonResultFactory = jsonResultFactory;
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.Result is JsonResult jsonResult)
{
var newResult = this.CreateJsonResult();
newResult.ContentEncoding = jsonResult.ContentEncoding;
newResult.ContentType = jsonResult.ContentType;
newResult.Data = jsonResult.Data;
newResult.JsonRequestBehavior = jsonResult.JsonRequestBehavior;
filterContext.Result = newResult;
}
base.OnActionExecuted(filterContext);
}
protected JsonResult CreateJsonResult()
{
if (this.JsonResultFactory != null)
{
return this.JsonResultFactory();
}
else if (DefaultJsonResultFactory != null)
{
return DefaultJsonResultFactory();
}
else
{
throw new NullReferenceException("Factoryプロパティ、又はDefaultFactoryプロパティが指定されていません。");
}
}
}
FilterConfigではJsonHandlerAttributeをグローバルフィルタとして登録するだけにして、Global.asax.csにデフォルトのオプション設定を記述します。
using System.Text.Json;
using System.Text.Json.Serialization;
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
... 略 ...
// グローバルフィルタとしてJsonHandlerAttributeを登録
filters.Add(new JsonHandlerAttribute()); // DefaultJsonResultFactoryを使用する
}
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
... 略 ...
// System.Text.Jsonのオプション設定
var serializerOptions = new JsonSerializerOptions(JsonSerializerDefault.Web)
{
NumberHandling =
JsonNumberHandling.AllowReadingFromString |
JsonNumberHandling.WriteAsString
};
var jsonResultFactory = ()=> new SystemTextJsonResult()
{
Settings = serializerOptions
};
// JsonHandlerAttributeの設定
JsonHandlerAttribute.DefaultJsonResultFactory = jsonResultFactory;
}
上記でJsonSerializerOptionsのインスタンスをファクトリ関数の中で毎回生成せずにシステム全体で共有しているのは、マイクロソフトのドキュメントに以下のように書かれている為です。
同じオプションで JsonSerializerOptions を繰り返し使用する場合、使用のたびに新しい JsonSerializerOptions インスタンスを作成しないでください。
どうやらJsonSerializerOptionsは内部でメタデータを生成する為、使うたびに生成するとコストがかかるようです。このメタデータのキャッシュはスレッドセーフなようなので、共有することにも問題はなさそうです。
ちなみに上記はSystem.Text.Jsonを使ってシリアライズ設定を外部に出していますが、Newtonsoft.Jsonの場合でもまったく同じJsonHandlerAttributeを使って以下のように設定できます。
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
... 略 ...
Newtonsoft.Json.JsonConvert.DefaultSettings = () =>
{
// JSON.NET設定クラス
var settings = new Newtonsoft.Json.JsonSerializerSettings();
// 列挙型を文字列にするコンバータの生成
var converter = new StringEnumConverter() {
NamingStrategy = new DefaultNamingStrategy() // 列挙型名をそのまま値とする
};
settings.Converters.Add(converter);
return settings;
};
// JsonHandlerAttributeの設定
JsonHandlerAttribute.DefaultJsonResultFactory = () => new JsonNetResult();
}
Newtonsoft.Jsonの場合、そもそもDefaultSettingsプロパティを使えば簡単にグローバル設定が行えるので、簡単ですね。
おまけ:ASP.NET MVC for VB.NET版
多くのASP.NET(.NET Framework版)システムではVB.NETが使われているでしょうから、JsonNetResult、SystemTextJsonResult、JsonHandlerAttribute、FilterConfig、列挙型の各定義について、VB.NET版も掲載します。
共通
''' <summary>
''' コントローラアクションの戻り値がJsonResultの場合、カスタムJsonResultクラスに置き換えるフィルタ
''' </summary>
Public Class JsonHandlerAttribute
Inherits ActionFilterAttribute
Public Property JsonResultFactory As Func(Of JsonResult) = Nothing
Public Shared Property DefaultJsonResultFactory As Func(Of JsonResult) = Nothing
Public Sub New()
MyBase.New()
End Sub
Public Sub New(jsonResultFactory As Func(Of JsonResult))
MyClass.New()
Me.JsonResultFactory = jsonResultFactory
End Sub
Public Overrides Sub OnActionExecuted(filterContext As ActionExecutedContext)
'JsonResultを置き換える。
Dim jsonResult = TryCast(filterContext.Result, JsonResult)
If jsonResult IsNot Nothing Then
Dim newResult = CreateJsonResult()
With newResult
.ContentEncoding = jsonResult.ContentEncoding
.ContentType = jsonResult.ContentType
.Data = jsonResult.Data
.JsonRequestBehavior = jsonResult.JsonRequestBehavior
End With
filterContext.Result = newResult
End If
MyBase.OnActionExecuted(filterContext)
End Sub
Protected Function CreateJsonResult() As JsonResult
If JsonResultFactory IsNot Nothing Then
Return Me.JsonResultFactory()()
ElseIf DefaultJsonResultFactory IsNot Nothing Then
Return DefaultJsonResultFactory()()
Else
Throw New NullReferenceException("Factoryプロパティ、又はDefaultFactoryプロパティが指定されていません。")
End If
End Function
End Class
Imports System.Web
Imports System.Web.Mvc
Public Module FilterConfig
Public Sub RegisterGlobalFilters(ByVal filters As GlobalFilterCollection)
... 略 ...
' JsonResultを差し替えるフィルタ
filters.Add(New JsonHandlerAttribute) 'DefaultJsonResultFactoryを使う
End Sub
End Module
Newtonsoft.Json 用
Imports Newtonsoft.Json
''' <summary>
''' JSON.NETを使用してJSONシリアライズするJsonResult
''' </summary>
Public Class JsonNetResult
Inherits JsonResult
Public Sub New()
Settings = Nothing
End Sub
Public Property Settings As JsonSerializerSettings
Public Overrides Sub ExecuteResult(context As ControllerContext)
' コンテキストが空
If (context Is Nothing) Then
Throw New ArgumentNullException("context")
End If
' GETが許可されていないのにGETで要求されている
If (Me.JsonRequestBehavior = JsonRequestBehavior.DenyGet AndAlso String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) Then
Throw New InvalidOperationException("JSON GET is not allowed")
End If
Dim response As HttpResponseBase = context.HttpContext.Response
response.ContentType = If(String.IsNullOrEmpty(Me.ContentType), "application/json", Me.ContentType)
If (Me.ContentEncoding IsNot Nothing) Then
response.ContentEncoding = Me.ContentEncoding
End If
If Me.Data Is Nothing Then
Exit Sub
End If
' JSON.NETを使ってシリアライズ
Dim scriptSerializer = If(Me.Settings Is Nothing, JsonSerializer.CreateDefault, JsonSerializer.Create(Me.Settings))
scriptSerializer.Serialize(response.Output, Me.Data)
'MyBase.ExecuteResult(context)
End Sub
End Class
<Newtonsoft.Json.JsonConverter(GetType(StringEnumConverter))>
Public Enum ResultType
NG
OK
End Enum
Protected Sub Application_Start()
... 略 ...
' Newtonsoft.Jsonのグローバル設定
Newtonsoft.Json.JsonConvert.DefaultSettings =
Function()
Dim settings = New Newtonsoft.Json.JsonSerializerSettings With {
.ContractResolver = New Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver,
.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
}
settings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
Return settings
End Function
' JsonHandlerAttributeの設定
JsonHandlerAttribute.DefaultJsonResultFactory = Function() New JsonNetResult
End Sub
System.Text.Json用
Imports System.Text.Json
Imports System.Text.Json.Serialization
''' <summary>
''' System.Text.Jsonを使用してJSONシリアライズするJsonResult
''' </summary>
Public Class SystemTextJsonResult
Inherits JsonResult
Public Sub New()
Settings = Nothing
End Sub
Public Property Settings As JsonSerializerOptions
Public Overrides Sub ExecuteResult(context As ControllerContext)
' コンテキストが空
If (context Is Nothing) Then
Throw New ArgumentNullException("context")
End If
' GETが許可されていないのにGETで要求されている
If (Me.JsonRequestBehavior = JsonRequestBehavior.DenyGet AndAlso String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) Then
Throw New InvalidOperationException("JSON GET is not allowed")
End If
Dim response As HttpResponseBase = context.HttpContext.Response
response.ContentType = If(String.IsNullOrEmpty(Me.ContentType), "application/json", Me.ContentType)
If (Me.ContentEncoding IsNot Nothing) Then
response.ContentEncoding = Me.ContentEncoding
End If
If Me.Data Is Nothing Then
Exit Sub
End If
' System.Text.Jsonを使ってシリアライズ
Dim stringValue = If(Me.Settings Is Nothing, JsonSerializer.Serialize(Me.Data), JsonSerializer.Serialize(Me.Data, Me.Settings))
response.Output.Write(stringValue)
End Sub
End Class
<System.Text.Json.Serialization.JsonConverter(GetType(System.Text.Json.Serialization.JsonStringEnumConverter))>
Public Enum ResultType
NG
OK
End Enum
Protected Sub Application_Start()
... 略 ...
' JsonHandlerAttributeが使用するSystem.Text.Jsonのグローバル設定
Dim serializerOptions = New JsonSerializerOptions(JsonSerializerDefaults.General) With
{
.NumberHandling =
JsonNumberHandling.AllowReadingFromString Or
JsonNumberHandling.WriteAsString
}
' JsonHandlerAttributeの設定
JsonHandlerAttribute.DefaultJsonResultFactory = Function() New SystemTextJsonResult With {.Settings = serializerOptions}
End Sub