[MVC]擴充JsonResult,自動處理Json或Jsonp的Request與轉型Json時的循環參考

有時寫一個Ajax Service,在寫的時候9成9都是自己網站用,那時多半不會考慮跨網站安全性問題,突然有其他同事說他也要用,就要回頭變動程式可以吃Jsonp,幾次下來就覺得要從根本解起,就寫了JsonPlusResult + ControllerPlus,由底層來處理這些事情,在開發的時候可以使用相同的習慣,做到多樣事情。

而內建的JsonConverter,在處理Json的Property轉換,遇到循環參考會出錯,但用ORM一定會遇到這問題,如Order.OrderDetails與OrderDetail.Order,這樣簡單的結構內建的JsonConverter就無法轉換了,後來改用Json.Net來處理轉換,也把這功能放入JsonPlusResult中。

有時寫一個Ajax Service,在寫的時候9成9都是自己網站用,那時多半不會考慮跨網站安全性問題,突然有其他同事說他也要用,就要回頭變動程式可以吃Jsonp,幾次下來就覺得要從根本解起,就寫了JsonPlusResult + ControllerPlus,由底層來處理這些事情,在開發的時候可以使用相同的習慣,做到多樣事情。

而內建的JsonConverter,在處理Json的Property轉換,遇到循環參考會出錯,但用ORM一定會遇到這問題,如Order.OrderDetails與OrderDetail.Order,這樣簡單的結構內建的JsonConverter就無法轉換了,後來改用Json.Net來處理轉換,也把這功能放入JsonPlusResult中。

 

Ajax跨網域安全性

Ajax跨網域安全性問題不是Server端抯擋跨網域的連線,而是用戶的Browser為了安全性去抯擋跨網域連線,為了決解這問題,之前查有發現三個解法:

 

  • 因為只有Broswer擋,所以可以Server對跨網域Server下載資料,前端在和同網域Server下載資料,但方法最囉嗦最麻煩,不建議使用。

 

  • CORS
    Cross-Origin Resource Sharing(CORS)是W3C的提案,可以在HttpHeader中設定那些跨網域的網站可以存取,但只限有實做XMLHttpRequest Level 2的Browser,如下IE8(Windows 7 version), Safari 4+, Chrome and Firefox 3.5+,很可惜的萬惡的IE6-8(XP version)不支援,所以就放棄這個選項。

實作方法如下:


public class AllowCrossSiteJsonAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.RequestContext.HttpContext.Response.AddHeader("Access-Control-Allow-Origin", "*");
        //或某網域
        //filterContext.RequestContext.HttpContext.Response.AddHeader("Access-Control-Allow-Origin", "http://mvc.tw");
        base.OnActionExecuting(filterContext);
    }
}

[AllowCrossSiteJson]
public ActionResult API()
{
    return Json(model);
}

//也可以在global.asax中增加,這樣可以不改變Controller,但前提是所有的Action都可以對外
protected void Application_BeginRequest(object sender, EventArgs e)
{
    HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*");
}

 

  • JSONP 
    JSONP其實是鑽Broswer的漏洞,因為 HTML <script> 標籤在瀏覽器裡不遵守同源策略,可以想像送一個跨網站Request,下載js檔,js檔中呼叫,事先寫好的function,而參數是物件字面量表示法(跟JSON是相同的格式),達到呼叫遠端並下載的作用,但ASP.NET MVC中沒有內建處理的ActionResult(可能是漏洞的關係吧),而且還要處理Callback,會比CORS麻煩,但本篇就是要教你如何簡化這些麻煩。

範例:


//js端

//先寫好callback
function myFunction(jsonData){
    //do something
}

//產生<script>標籤
document.write("<script src='http://CrossDomainSite/api?jsonCallback=myFunction'></script>");


//Server端
public ActionResult Api(string jsonCallback)
{
    //回傳的是Javascript
    return Javascript("{0}({1});", jsonCallback, model.ToJson());
}

//回傳的資料
<script>
//會呼叫事先寫好的function
myFunction( { Name:"Wade", Site:"Mvc.Tw" } );
</script>


//如果用jQuery不用那麼麻煩
$.ajax({
    type:"post",
    url:"http://localhost:30881/JsonTest/Data",
    dataType : "jsonp", //只要設定jsonp就會幫你處理function與url與&lt;script&gt;
    success:function (data) {
        //do something
    },
});

 

產生的Url
image

 

JsonPlusReslut原始碼


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;

//可以將ObjectExtensions與ControllerPlus可以移到你自己相關的Code中
namespace System
{
    public static class ObjectExtensions
    {
        /// <summary>
        /// 轉成Json格式
        /// </summary>
        /// <returns></returns>
        public static string ToJson(this object target)
        {
            return JsonConvert.SerializeObject(target, Formatting.None);
        }
    }
}

namespace System.Web.Mvc
{
    public class ControllerPlus : Controller
    {
        //覆寫Controller.Json方法,回傳JsonPlusResult
        protected override JsonResult Json(object data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior)
        {
            return new JsonPlusResult(data) { ContentType = contentType, ContentEncoding = contentEncoding, JsonRequestBehavior = behavior };
        }
    }

    /// <summary>
    /// 處理Json與Jsonp的Request與Json的轉換並解決循環參考
    /// </summary>
    public class JsonPlusResult : JsonResult
    {
        public JsonPlusResult(object model)
        {
            this.Data = model;
        }

        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("DenyGet");
            }

            //Json Padding的callback
            string jsoncallback = GetCallbackName(context);

            //ContentType
            HttpResponseBase response = context.HttpContext.Response;

            if (this.ContentEncoding != null)
            {
                response.ContentEncoding = this.ContentEncoding;
            }

            //

            if (!string.IsNullOrEmpty(this.ContentType))
            {
                response.ContentType = this.ContentType;
            }
            else
            {
                if (string.IsNullOrEmpty(jsoncallback))
                {
                    response.ContentType = "application/json";
                }
                else
                {
                    response.ContentType = "application/x-javascript"; //json Padding
                }
            }

            string dataToJson = this.Data.ToJson();

            if (response.ContentType == "application/x-javascript")
            {
                response.Write(string.Format("{0}({1})", jsoncallback, dataToJson)); //json Padding
            }
            else
            {
                response.Write(dataToJson);
            }
        }

        private static string GetCallbackName(ControllerContext context)
        {
            string result = context.HttpContext.Request["jsoncallback"] as string;
            if (!string.IsNullOrWhiteSpace(result))
            {
                return result;
            }

            result = context.HttpContext.Request["callback"] as string;
            if (!string.IsNullOrWhiteSpace(result))
            {
                return result;
            }

            return null;
        }
    }
}

 

使用方法


//可以使用繼承ControllerPlus
public class JsonTestController : ControllerPlus //繼承
{
    public ActionResult Index()
    {
        //跟以前一樣
        return Json(model);
    }
}

//也可以直接使用JsonPlusResult
public class JsonTestController : Controller //不繼承
{
    public ActionResult Index()
    {
        return new JsonPlusResult(model);
    }
}

 

qunit測試結果

image


<script type="text/javascript">
//json
$(function () {
    test("json", function () {
        $.ajax({
            async:false,
            type:"post",
            url:"@Url.Action("Data", "JsonTest")",
            dataType : "json",
            success:function (data) {
                equal(data.Name, "Wade", "Json資料比對");
            },
            error:function (xhr,status,errorThrown) {
                ok(false, errorThrown);
            }
        });
    });

    test("jsonp auto callback", function () {
        $.ajax({
            async:false,
            type:"post",
            url:"@Url.Action("Data", "JsonTest")" ,
            dataType : "jsonp",
            success:function (data) {
                equal(data.Name, "Wade", "Json資料比對");
            },
            error:function (xhr,status,errorThrown) {
                ok(false, errorThrown);
            }
        });
    });

    test("jsonp custom callback", function () {
        $.ajax({
            async:false,
            type:"post",
            url:"@Url.Action("Data", "JsonTest")",
            jsonpCallback:"customJsonpCallback",
            dataType : "jsonp",
            error:function (xhr,status,errorThrown) {
                ok(false, errorThrown);
            }
        });
    });
});

function customJsonpCallback(data, status) {
    equal(data.Name, "Wade", "Json資料比對");
}
</script>

 

JsonPlsuResult.cs

 

參考文件

XMLHttpRequest執行AJAX 跨網域存取

Cross-Origin requests and ASP.NET MVC

JSONP - 维基百科