愛流浪的小風

技術隨手寫

使用Asp.Net MVC打造Web Api (18) - 使用Json.Net驗證JSON格式是否正確

| Comments

在我們提供Api給使用者作操作時,經常還會遇到一個是Json格式的不正確,比如說結尾少了}符號,或是應該傳入字串的欄位傳成數字等等,都有可能造成Api的操作失敗,而對使用者而言可能會不太容易判斷目前的操作是因為資料內容的錯誤,亦或是Json格式的錯誤導致,因此透過將資料進行簡單的JsonSchema驗證,可以避免掉大部分的Json格式異常,並讓使用者可以容易判斷錯誤的地方,今天就將向大家介紹如何使用Json.Net進行簡單的JsonSchema驗證。

大家可以從Github ApiSample - Tag Day17開始今天的練習

建立簡單的Api測試頁面

由於我們在之前的文章中已經替Api加上了驗證程式,另外為了模擬不同語言針對Api操作時可能會遇到的問題,因此我們將建立簡單的Api測試頁面,來實際測試並模擬Api操作的結果。

  1. 使用Nuget加入bootstrap、knockout等Library,並安裝TypeScript(這是一種可compile成javascript的語言,並提供Visual studio 2012的編輯器支援)

    TypeScript官方網站

  2. 在網站建立UtilsController,並包含加密、取得時間戳記,以及Api測試頁面

    public class UtilController : Controller
    {
        public IChiperTextHelper ChiperTextHelper { get; set; }
    
        public UtilController(IChiperTextHelper chiperTextHelper)
        {
            this.ChiperTextHelper = chiperTextHelper;
        }
    
        public ActionResult Helper()
        {
            return View();
        }
    
        public string GetTimeStamp()
        {
            return this.ChiperTextHelper.GetTimeStamp();
        }
    
        public string GetSignature(string key, string saltKey, string timeStamp, string data)
        {
            return this.ChiperTextHelper.GetSignature(key, saltKey, timeStamp, data);
        }
    
    }
    
  3. 修改BundleConfig,加入Bootstrap和knockout的路徑

    bundles.Add(new ScriptBundle("~/bundles/knockout").Include("~/Scripts/knockout-{version}.js"));
    
    bundles.Add(new StyleBundle("~/Content/bootstrap/css").Include("~/Content/bootstrap/bootstrap.css"));
    
  4. 修改_Layout.cshtml,加入bootstrap和knockout的連結

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <title>@ViewBag.Title</title>
        @*@Styles.Render("~/Content/css")*@
        @Styles.Render("~/Content/bootstrap/css")
        @Scripts.Render("~/bundles/modernizr")
    </head>
    <body>
        @RenderBody()
    
        @Scripts.Render("~/bundles/jquery")
        @Scripts.Render("~/bundles/knockout")
        @RenderSection("scripts", required: false)
    </body>
    </html>
    
  5. 新增utilshelper.ts,撰寫Api測試工具的操作語法

    註: 需先使用nuget加入jquery.d.ts和knockout.d.ts

    /// <reference path="..\typings\jquery\jquery.d.ts" />
    /// <reference path="..\typings\knockout\knockout.d.ts" />
    
    module ApiSample {
        export class UtilService {
            GetTimeStamp() {
                var dfd = $.Deferred();
    
                $.ajax({
                    url: '/Util/GetTimeStamp',
                    type: 'GET',
                    contentType: 'application/json',
                    success: (result) => {
                        dfd.resolve(result);
                    },
                    error: (ex) => {
                        dfd.reject(ex);
                    }
                });
    
                return dfd.promise();
            }
    
            GetSignature(key: string, saltkey: string, timestamp: any, data: string) {
                var dfd = $.Deferred();
    
                $.ajax({
                    url: '/Util/GetSignature', 
                    type: 'GET',
                    contentType: 'application/json',
                    data: { key: key, saltkey: saltkey, timestamp: timestamp, data: data },
                    success: (result) => {
                        dfd.resolve(result);
                    },
                    error: (ex) => {
                        dfd.reject(ex);
                    }
                });
    
                return dfd.promise();
            }
        }
        export class UtilHelper {
            service: UtilService;
    
            url: KnockoutObservable<string>;
    
            key: KnockoutObservable<string>;
    
            saltkey: KnockoutObservable<string>;
    
            token: KnockoutObservable<string>;
    
            data: KnockoutObservable<string>;
    
            result: KnockoutObservable<string>;
    
            constructor() {
                this.url = ko.observable();                    
                this.key = ko.observable();
                this.saltkey = ko.observable();
                this.token = ko.observable();
                this.data = ko.observable();                    
                this.result = ko.observable();
    
                this.service = new UtilService();
    
                this.Query = () => {
                    this.service.GetTimeStamp()
                        .done((timeStamp) => {
                            this.service.GetSignature(this.key(), this.saltkey(), timeStamp, this.data())
                                .done((signature) => {
                                    $.ajax({
                                        url: this.url(),
                                        type: 'POST',
                                        contentType: 'application/json',
                                        data: ko.toJSON({ token: this.token(), timestamp: timeStamp, signature: signature, data: this.data() }),
                                        success: (result) => {                                        
                                            this.result('<pre>' + JSON.stringify(ko.toJS(result), null, '  ') + '</pre>');
                                        },
                                        error: (ex) => {
                                            this.result('<pre>' + JSON.stringify(JSON.parse(ex.responseText), null, '  ') + '</pre>');
                                        }
                                    });
                                });
                        });
                }
            }
    
            Query: Function;
        }
    }
    
    var helper = new ApiSample.UtilHelper();
    ko.applyBindings(helper);
    
  6. 新增Utils/Helper.cshtml,Api測試工具畫面

    @{
        ViewBag.Title = "Helper";
        Layout = "~/Views/Shared/_Layout.cshtml";
    }
    
    <div class="container">
        <h1>Api Helper
        </h1>
        <form class="form-horizontal" role="form">
            <div class="form-group">
                <label for="url" class="col-lg-2 control-label">Url</label>
                <div class="col-lg-10">
                    <input type="text" class="form-control" id="url" placeholder="Url" data-bind="value: url">
                </div>
            </div>
            <div class="form-group">
                <label for="token" class="col-lg-2 control-label">Token</label>
                <div class="col-lg-10">
                    <input type="text" class="form-control" id="token" placeholder="Api Token" data-bind="value: token">
                </div>
            </div>
            <div class="form-group">
                <label for="key" class="col-lg-2 control-label">Key</label>
                <div class="col-lg-10">
                    <input type="text" class="form-control" id="key" placeholder="Api Key" data-bind="value: key">
                </div>
            </div>
            <div class="form-group">
                <label for="saltkey" class="col-lg-2 control-label">SaltKey</label>
                <div class="col-lg-10">
                    <input type="text" class="form-control" id="saltkey" placeholder="Api SaltKey" data-bind="value: saltkey">
                </div>
            </div>
            <div class="form-group">
                <label for="data" class="col-lg-2 control-label">Data</label>
                <div class="col-lg-10">
                    @*<input type="text" class="form-control" id="data" placeholder="Data">*@
                    <textarea class="form-control" rows="10" data-bind="value: data"></textarea>
                </div>
            </div>
            <div class="form-group">
                <div class="col-lg-offset-2 col-lg-10">
                    <button type="button" class="btn btn-primary" data-bind="click: Query">Query</button>
                </div>
            </div>
        </form>
        <hr />
        <div data-bind="html: result">
        </div>
    </div>
    
    @section scripts{
        <script src="~/Scripts/app/utilhelper.js"></script>
    }
    
  7. 執行Api測試頁面,可以看到畫面,我們可以試著輸入資料進行查詢

模擬Json格式錯誤

我們實際模擬一下,如果在新增商品時,輸入了錯誤的Json格式,會在Api得到甚麼樣的錯誤訊息,例如我將其中的一個逗號拿掉

我們發現所得到的資訊是Api無法辨認輸入的Data是Json,也沒辦法將其還原為.Net的Model,所以Api是為輸入了一個空的物件,使用者可能也會覺得奇怪,因為他明明輸入了所有欄位的資料,卻得到這樣的訊息,因此我們將改造一下我們的Api,提供檢查JsonSchema的功能,作一個基本的防護並提供錯誤資訊

延伸閱讀:

新增JsonSchema驗證

我們新增一個ActionFilter,來替需要輸入比較複雜資料的Action多進行一層檢查,來避免Json格式異常產生的問題

  1. 在Extensions新增JsonSchemaNotValidException,用來拋出格式錯誤的異常

    [Serializable]
    public class JsonSchemaNotValidException : Exception
    {
        public JsonSchemaNotValidException() { }
        public JsonSchemaNotValidException(string message) : base(message) { }
        public JsonSchemaNotValidException(string message, Exception inner) : base(message, inner) { }
        protected JsonSchemaNotValidException(
          System.Runtime.Serialization.SerializationInfo info,
          System.Runtime.Serialization.StreamingContext context)
            : base(info, context) { }
    }
    
  2. 在Extensions新增ValidateJsonSchemaAttribute,檢查輸入的資料

    public class ValidateJsonSchemaAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var parameterDescriptors = filterContext.ActionDescriptor.GetParameters();
    
            //// Get json data
            var rawValue = filterContext.Controller.ValueProvider.GetValue("data");
            var rawData = string.Empty;
            if (rawValue == null)
            {
                return;
            }
            else
            {
                rawData = rawValue.AttemptedValue;
            }
    
            //// Check Parameter type
            if (parameterDescriptors.Count() != 1 || parameterDescriptors.First().ParameterType.IsValueType)
            {
                return;
            }
    
            //// Get json schema from parameter
            JsonSchema jsonSchema = null;
            var parameterDescriptor = parameterDescriptors.First();
            var jsonSchemaGenerator = new JsonSchemaGenerator();
            jsonSchema = jsonSchemaGenerator.Generate(parameterDescriptor.ParameterType);
            jsonSchema.Title = parameterDescriptor.ParameterType.Name;
    
            jsonSchema = JsonSchema.Parse(jsonSchema.ToString().ToLower());
    
            var errs = new List<string>() as IList<string>;
            JObject jsonObject = null;
    
            //// 處理Json格式的異常
            try
            {
                jsonObject = JObject.Parse(rawData.ToLower());
            }
            catch (JsonReaderException ex)
            {
                throw new JsonSchemaNotValidException(ex.Message, ex);
            }
    
            var valid = jsonObject.IsValid(jsonSchema, out errs);
            if (errs.Count > 0)
            {
                throw new JsonSchemaNotValidException("請確認傳入資料型別是否符合規範!" + 
                    string.Join(",", errs.ToArray()));
            }
        }
    }
    
  3. 修改web.config,允許JsonSchema錯誤訊息顯示在Api回傳上

  4. 重新在Api查詢,我們發現Json格式錯誤會提供比較有幫助的錯誤訊息了!

本日小結

不斷的透過小幅度的修改,來完善我們Api的內容,並且持續提供使用者更好的使用體驗,才能夠讓產品更加的受歡迎,透過一個簡單的擴充功能,就可以讓操作上的錯誤判斷更加的容易,也是很重要的,當然沒有絕對完美的東西,還是要再根據使用情境持續的調整程式碼才行!關於今天的內容,歡迎大家一起討論喔^_^

Comments

comments powered by Disqus