LoginSignup
3
6

More than 1 year has passed since last update.

[ASP.NET MVC]【徹底解説】JsonResultが生成する列挙型の値を数値ではなく列挙型名でシリアル化する

Last updated at Posted at 2021-08-24

ASP.NET MVC(及びASP.NET MVC Core)にて、JsonResultを返すコントローラアクションがあったとします。

この時、返されるオブジェクトのプロパティにEnumがある場合、数値ではなく列挙型名でシリアル化する方法をまとめます。

ResultType.OK という列挙型が、"1" ではなく "OK"という文字でシリアル化できるようになります。

こんな列挙型プロパティを持つViewModelをJsonResultで返す時に…
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が生成する列挙型の値を数値ではなく列挙型名でシリアル化する。
  • システム全体のグローバル設定とし、コントローラ側で意識しなくて良いようにする。

この記事でわかること

  1. ASP.NET MVC Core(.NET Core/5+)でSystem.Text.Jsonを使って列挙型を文字列にする。
  2. ASP.NET MVC(.NET Framework)でNewtonsoft.Jsonを使って列挙型を文字列にする。また、それをグローバルに設定する。
  3. ASP.NET MVC(.NET Framework)でSystem.Text.Jsonを使って列挙型を文字列にする。また、それをグローバルに設定する。
  4. 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句
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"のように先頭が小文字のプロパティ名に置き換えられます。

これが困る場合、システム全体の設定として、キャメルケースをオフにすることもできます。

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews().AddJsonOptions(opts =>
    {
        // PropertyNamingPolicyをnullに設定するとプロパティ名の変換が行われない。
        opts.JsonSerializerOptions.PropertyNamingPolicy = null; // default: System.Text.Json.JsonNamingPolicy.CamelCase;
    });
}

もし、システム全体の設定として一律に列挙型の値を列挙型名にしたい場合は次のようにすることもできます。
が、あまり使うことはないでしょう。

Startup.cs
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句
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句
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 に共通の設定を保存します。
こうすることで、システム全体で設定を共有できます。

Global.asax.cs
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の説明です。

JsonNetResult.cs
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以外の場合には何も行いません。

JsonHandlerAttribute.cs
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が呼ばれるようになります。

FilterConfig.cs
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を作ります。

SystemTextJsonResult.cs
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を使うように変更します。

JsonHandlerAttribute.cs
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をグローバルフィルタとして登録します。

FilterConfig.cs
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の中で行うのがとりあえず手っ取り早いでしょう。

SystemTextJsonResult.cs
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のソースコードから引用します。

JsonSerializerOptions.cs(GitHubより)
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に置き換えただけです。

JsonHandlerAttribute.cs(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 を追加します。

JsonHandlerAttribute.cs
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にデフォルトのオプション設定を記述します。

FilterConfig.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を使用する

    }
}
Global.asax
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を使って以下のように設定できます。

Global.asax
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版も掲載します。

共通

JsonHandlerAttribute.vb
''' <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
FilterConfig.vb
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 用

JsonNetResult.vb
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の場合)
<Newtonsoft.Json.JsonConverter(GetType(StringEnumConverter))>
Public Enum ResultType
    NG
    OK
End Enum
Global.asax.vb(Newtonsoft.Jsonの場合)
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用

SystemTextJsonResult.vb
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の場合)
<System.Text.Json.Serialization.JsonConverter(GetType(System.Text.Json.Serialization.JsonStringEnumConverter))>
Public Enum ResultType
    NG
    OK
End Enum
Global.asax.vb(Newtonsoft.Jsonの場合)
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
3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6