2017年12月5日 星期二

以 Json.Net 取代預設的序列化及反序列化行為

緣由
透過 $http.post 呼叫 action 並帶入參數,但參數值較大(例如檔案內容),會遇到如下訊息
使用 JSON JavaScriptSerializer 序列化或還原序列化期間發生錯誤。字串的長度超過在 maxJsonLength 屬性上設定的值

好處
1. 效能較快
2. action 輸入跟輸出不會有長度限制

作法
1. 建立 JsonNetValueProviderFactory
public class JsonNetValueProviderFactory : ValueProviderFactory
    {
        public override IValueProvider GetValueProvider(ControllerContext controllerContext)
        {
            // first make sure we have a valid context
            if (controllerContext == null)
                throw new ArgumentNullException("controllerContext");

            // now make sure we are dealing with a json request
            if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
                return null;

            // get a generic stream reader (get reader for the http stream)
            StreamReader streamReader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
            // convert stream reader to a JSON Text Reader
            JsonTextReader JSONReader = new JsonTextReader(streamReader);
            // tell JSON to read
            if (!JSONReader.Read())
                return null;

            // make a new Json serializer
            JsonSerializer JSONSerializer = new JsonSerializer() { DateTimeZoneHandling= DateTimeZoneHandling.Local };
            // add the dyamic object converter to our serializer
            JSONSerializer.Converters.Add(new ExpandoObjectConverter());

            // use JSON.NET to deserialize object to a dynamic (expando) object
            Object JSONObject;
            // if we start with a "[", treat this as an array
            if (JSONReader.TokenType == JsonToken.StartArray)
                JSONObject = JSONSerializer.Deserialize>(JSONReader);
            else
                JSONObject = JSONSerializer.Deserialize(JSONReader);

            // create a backing store to hold all properties for this deserialization
            Dictionary backingStore = new Dictionary(StringComparer.OrdinalIgnoreCase);
            // add all properties to this backing store
            AddToBackingStore(backingStore, String.Empty, JSONObject);
            // return the object in a dictionary value provider so the MVC understands it
            return new DictionaryValueProvider < object >(backingStore, CultureInfo.CurrentCulture);
        }

        private static void AddToBackingStore(Dictionary backingStore, string prefix, object value)
        {
            IDictionary d = value as IDictionary;
            if (d != null)
            {
                foreach (KeyValuePair entry in d)
                {
                    AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
                }
                return;
            }

            IList l = value as IList;
            if (l != null)
            {
                for (int i = 0; i < l.Count; i++)
                {
                    AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
                }
                return;
            }

            // primitive
            backingStore[prefix] = value;
        }

        private static string MakeArrayKey(string prefix, int index)
        {
            return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
        }

        private static string MakePropertyKey(string prefix, string propertyName)
        {
            return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
        }
    }

2. Global.asax.cs Application_Start() 增加如下內容
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
            ValueProviderFactories.Factories.Add(new JsonNetValueProviderFactory());         

3. 建立 JsonNetResult.cs
public class JsonNetResult : JsonResult
    {
        public JsonSerializerSettings SerializerSettings { get; set; }
        public Formatting Formatting { get; set; }
        public JsonNetResult()
        {
            SerializerSettings = new JsonSerializerSettings();
        }
        public override void ExecuteResult(ControllerContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");
            HttpResponseBase response = context.HttpContext.Response;
            response.ContentType =
                !string.IsNullOrEmpty(ContentType) ? ContentType : "application/json";
            if (ContentEncoding != null)
                response.ContentEncoding = ContentEncoding;
            if (Data != null)
            {
                JsonTextWriter writer = new JsonTextWriter(response.Output)
                {
                    Formatting = Formatting,
                };
                JsonSerializer serializer = JsonSerializer.Create(SerializerSettings);
                serializer.Serialize(writer, Data); writer.Flush();
            }
        }
    }

4. 建立 JsonNetController
 public class JsonNetController : Controller
    {
        protected override JsonResult Json(object data, string contentType=null,
                  System.Text.Encoding contentEncoding=null, JsonRequestBehavior behavior= JsonRequestBehavior.AllowGet)
        {
            if (behavior == JsonRequestBehavior.DenyGet
                && string.Equals(this.Request.HttpMethod, "GET",
                                 StringComparison.OrdinalIgnoreCase))
                //Call JsonResult to throw the same exception as JsonResult
                return new JsonResult();
            return new JsonNetResult()
            {
                Data = data,
                ContentType = contentType,
                ContentEncoding = contentEncoding, 
                SerializerSettings= new Newtonsoft.Json.JsonSerializerSettings() { DateTimeZoneHandling= Newtonsoft.Json.DateTimeZoneHandling.Local }
            };
        }     
    }
並讓繼承Controller 的改為繼承JsonNetController 

說明
1. 輸出 json 方式不變,但會改用 json.net 處理
return Json(list);
2. 輸入和輸出預設會考慮本地時區,不需要刻意設定
3. 若還是出現錯誤訊息 "超出最大的要求長度 ",則修改 web.config 的 httpRuntime,加入 maxRequestLength="2147483647"

Entity Framework 建立新物件並儲存後馬上取得關聯資料

使用 CreateProxy 建立物件,不要直接 new var newmodel = _contextXXX.CreateProxy<yyy>(); ... _contextXXX.yyy.Add(newmodel); await _contextXXX.SaveC...