LoginSignup
7
7

More than 5 years have passed since last update.

ASP.NET CoreでREST APIを作る際のJsonResult拡張クラス

Posted at

概要

ASP.NET CoreでREST APIを作るときに使えそうな、JsonResultの拡張クラスを作りました。

ASP.NET CoreデフォルトのJsonResultはそれだけでも便利なのですが、REST APIで使うときには、ちょっと足りないところがあり、結局自分で色々拡張しないといけません。

  • オブジェクトを上手い事Json形式にシリアライズしないといけない(時刻表現やエンコードが云々)
  • セキュリティ的なレスポンスヘッダーを付けないといけない

API仕様

  • キャッシュはさせない
    • 描画などキャッシュが重要なリッチな画面には使われない想定
  • レスポンスのフォーマット
    • エンベロープは利用しない
    • 基本的にエスケープはしない
      • エスケープしてしまうと、クライアント側で戻す必要があるが、クライアントは環境条件が多岐に渡るため、実装コストが高い
      • 必要なJsonコントロール文字(改行とかダブルクォートとか)のみエスケープ
      • "<" ">"などをエスケープしない代わりに、UTF-7攻撃対策として、レスポンスヘッダにcharset=utf-8を明示する。
      • エラー時は RFC7807 に従う https://www.eisbahn.jp/yoichiro/2017/01/rfc_7807.html

実装

環境

  • Visual Studio 2017
  • .NET Core 2.1
  • ASP.NET Core 2.1.1

コード

CustomJsonResult.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace WebApiAuthSample
{
    /// <summary>
    /// Json形式のレスポンスを返すためのActionResultクラス
    /// </summary>
    public class CustomJsonResult : JsonResult
    {
        /// <summary>
        /// 日付フォーマット。
        /// yyyy-MM-dd'T'HH:mm:ss.ffK(UTCだとZになる)
        /// </summary>
        private const string DateFormat = "yyyy-MM-dd'T'HH:mm:ss.ffK";

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="code">ステータスコード</param>
        /// <param name="data">データ</param>
        public CustomJsonResult(HttpStatusCode code, object data)
            : base(data)
        {
            base.StatusCode = (int)code;
        }

        /// <summary>
        /// MVCのアクションメソッドの結果を処理
        /// </summary>
        /// <param name="context">実行コンテキスト</param>
        /// <inheritdoc />
        public async override Task ExecuteResultAsync(ActionContext context)
        {
            if (context == null)
            {
                throw new UnauthorizedAccessException("The context of this HTTP request is not defined.");
            }

            HttpResponse response = context.HttpContext.Response;
            await SerializeJsonAsync(response);
        }

        /// <summary>
        /// 指定されたデータをJSONにシリアライズしレスポンスに格納
        /// </summary>
        /// <param name="response">格納するレスポンス</param>
        public async Task SerializeJsonAsync(HttpResponse response)
        {
            if (!String.IsNullOrEmpty(ContentType))
            {
                //MIME設定
                response.ContentType = ContentType;
            }
            else
            {
                response.ContentType = "application/json; charset=utf-8";
            }

            // Content Sniffering 対策
            response.Headers.Add("X-Content-Type-Options", "nosniff");

            // キャッシュ回避
            response.Headers.Add("Pragma", "no-cache");
            response.Headers.Add("Cache-Control", "no-store, no-cache");

            // クロスサイトスクリプティング防御機構を有効化
            response.Headers.Add("X-XSS-Protection", "1; mode=block");

            //CORS設定。クロスドメインアクセスが必要なら、適宜設定する。
            if (response.Headers.ContainsKey("Access-Control-Allow-Origin"))
            {
                response.Headers["Access-Control-Allow-Origin"] = "*";
            }
            else
            {
                response.Headers.Add("Access-Control-Allow-Origin", "*");
            }
            //response.Headers.Add("Access-Control-Allow-Credentials", "true");
            //response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Date, X-Api-Version, X-File-Name");
            //response.Headers.Add("Access-Control-Allow-Methods", "POST,GET,PUT,PATCH,DELETE,OPTIONS");

            // HTTPS対応
            response.Headers.Add("Strict-Transport-Security", "max-age=15768000");

            response.StatusCode = StatusCode == null ? StatusCodes.Status200OK : StatusCode.Value;
            if (Value != null)
            {
                // Json.NETでシリアライズ
                var converter = new IsoDateTimeConverter();
                converter.DateTimeStyles = System.Globalization.DateTimeStyles.AdjustToUniversal; //時刻はUTCで
                converter.DateTimeFormat = DateFormat;

                // 結果をレスポンスボディに書き込み
                await response.WriteAsync(JsonConvert.SerializeObject(
                    Value, new JsonSerializerSettings()
                    {
                        ContractResolver = new CamelCasePropertyNamesContractResolver(),
                        Converters = new List<JsonConverter>() { converter },
                        Formatting = Formatting.Indented,
                        StringEscapeHandling = StringEscapeHandling.Default,
                    }),
                    Encoding.UTF8 //指定しなくてもデフォルトでUTF8になるが、念のため明記
                );
                return;
            }
            else if (response.StatusCode != StatusCodes.Status204NoContent) //NoContentは結果を書けないので、それ以外の場合だけ空で埋める
            {
                await response.WriteAsync("", Encoding.UTF8);
            }
            return;
        }
    }
}

使い方

サーバ側の実装

ValuesController.cs
        public IActionResult Get(int? id)
        {
            if(id == null)
            {
                return new CustomJsonResult(HttpStatusCode.BadRequest, new
                {
                    Type = this.GetType().FullName,
                    Title = "The Access code is expired or invalid.",
                    Instance = Request?.Path.Value
                });
            }

            var result = new
            {
                Id = id,
                Value = id.ToString()
            };

            return new CustomJsonResult(HttpStatusCode.OK, result);
        }

実行結果

PS Z:\> Invoke-RestMethod -Uri "http://localhost:2192/api/values/1" -Method GET | ConvertTo-JSON
{
    "id":  1,
    "value":  "1"
}

仕様決めの参考にしたサイト

7
7
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
7
7