65.9K
CodeProject 正在变化。 阅读更多。
Home

在托管代码中反序列化 Microsoft Ajax ClientScript JSON

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (8投票s)

2010 年 4 月 13 日

CPOL

4分钟阅读

viewsIcon

33306

将 {"d":{"__type": ... JSON 响应无缝反序列化为 CLR 类型。

概述

上一篇文章中,我提出了一种反序列化来自 ClientScript 端点的 JSON 的方法,例如带有 [ScriptService] 或 [ScriptMethod] 属性的 XML WebServices、支持 Ajax 的 WCF 服务以及使用 WebScriptServiceHostFactory 创建的 WCF 服务。

促成这一需求的使用场景是测试由客户端 JavaScript 调用的端点。使用 HttpWebRequest 在托管代码中调用 WebScript 端点很容易,关键在于指定 content-type 'application/json'。而问题也随之而来。

问题领域

当 WebScript 端点以 content-type 'application/json' 响应请求时,它会理所当然地认为它是由 JavaScript 调用,并且(自 3.5sp1 起)会将实际的返回值包装在一个名为 'd' 的对象中,并为每个对象添加一个 '__type' 属性。

列表 1:.NET 3.5sp1 JSON 响应

{
    "d": {
        "__type": "Result:#HttpLibArticleSite",
        "Message": "Value pull OK.",
        "Session": "rmyykw45zbkxxxzdun0juyfr",
        "Value": "foo"
    }
}

一个解决方案

在托管代码中反序列化此 JSON 会带来一些挑战。

第一个是 'd' 包装器。你可以创建包装器类来处理所有你期望反序列化的类型,或者,如上一篇文章所示,创建一个通用的包装器,类似于下面的列表。

列表 2:Ajax 包装器

public class AjaxWrapper<T>
{
    public T d;   
}

下一个挑战是,如果没有自定义的 JavaScriptTypeResolver 来将 '__type' 值解析为托管类型,那么 JavaScriptSerializerDataContractJsonSerializer 都无法解析该 JSON。尝试这样做会导致 `ArgumentNull` 异常,因为它找不到 `JavaScriptTypeResolver` 来解析 '__type' 值。这与我们作为 .Serialize<T> 的泛型参数提供的类型无关。只有当 JSON 未被 `JavaScriptSerializer.ServerTypeFieldName (__type)` 装饰时,类型参数才会被用来实例化 JSON 被填充进去的实例。

我们可以创建一个自定义的 JavaScriptTypeResolver,但这将涉及解析类型并维护已注册类型的列表,就像运行时所做的那样。这完全超出了我们需求的预期范围。我们只是希望在处理简单的 JSON 时,能够享受默认 JavaScriptSerializer 的行为带来的便利。

因此,一种选择是使用 JSON 库,例如 JSON.net,它不识别 '__type' 属性,并能愉快地将包装的 JSON 反序列化到 AjaxWrapper 类中。

列表 3:使用 JSON.Net 和 AjaxWrapper

HttpLibArticleSite.Result result = Newtonsoft.Json.JsonConvert.DeserializeObject
	<AjaxWrapper<HttpLibArticleSite.Result>>(wrappedJson).d;

如果你已经在项目中使用了 JSON.NET,或者不介意依赖它,那么一切就绪。

更好的解决方案

最近,在开发一个用于测试 JSON 端点的新库时,对 JSON.NET 的依赖变得不那么理想,我开始了又一次尝试,仅使用我自己的代码和内置的框架类型来反序列化包装的 JSON。

如前所述,使用 .NET 序列化器(无论是否带包装器)解析包装的 JSON,如果没有自定义的 JavaScriptTypeResolver,就会失败。由于我们已经确定自定义 JavaScriptTypeResolver 不符合我们的需求,因此需要另一种方法。

障碍已经确定

  • d”包装器
  • __type”属性

显而易见的解决方案是消除 JSON 中不符合要求的文本。通常,我发现操纵文本是一项危险的任务,但在此情况下,文本符合 JSON 规范,因此使用 Regex 可以自信地满足要求。

提取内部 JSON 后,我们可以将“__type”替换为空字符串,然后将 JSON 反序列化到任何形状相似的 CLR 类型。

下面列出了最终的解决方案。ClientScriptJsonUtilities 是一个 static 类,它为 JavaScriptSerializer 提供了一个扩展方法,顺便说一句,该方法在 3.5sp1 中已被取消标记。

注意

某些端点配置会省略“__type”属性,但仍然使用“d”进行包装;而其他配置则会返回一个“裸”结果,不包装在“d”中,但仍包含“__type”字段;还有一些配置会返回 POJO JSON。

此类将正确处理这些类型的返回,因此 CleanAndDeserialize<T> 可以被视为 `Deserialize<T>` 的向后兼容替代品,在你的代码中随处可用。

匿名类型

现在考虑一个场景,你需要反序列化一个没有对应 CLR 类型的 JSON。你可以简单地定义一个与 JSON 形状相同的类,然后将 JSON 反序列化到该类中。

在需要频繁使用此类型或需要传递响应的情况下,这可能是最佳方法。其他时候,当你要定义的类可以被视为临时类时,使用匿名类型的方法可能更灵活。

Jacob Carpenter 的博客的帮助下,我为 CleanAndDeserialize 添加了一个重载,它将接受一个匿名原型。请参阅列表 6。

列表 4:ClientScriptJsonUtilities.cs

// /*!
//  * Project: Salient.Web.HttpLib
//  * http://salient.codeplex.com
//  */

#region

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Script.Serialization;

#endregion

namespace Salient.Web.HttpLib
{
    public static class ClientScriptJsonUtilities
    {
        private static readonly Regex RxMsAjaxJsonInner = 
	new Regex("^{\\s*\"d\"\\s*:(.*)}$", RegexOptions.Compiled);

        private static readonly Regex RxMsAjaxJsonInnerType = 
	new Regex("\\s*\"__type\"\\s*:\\s*\"[^\"]*\"\\s*,\\s*", RegexOptions.Compiled);

        /// <summary>
        /// Pre-processes <paramref name="json"/>, if necessary, 
        /// to extract the inner object from a "d:" 
        /// wrapped MsAjax response and removing "__type" 
        /// properties to allow deserialization with JavaScriptSerializer
        /// into an instance of <typeparamref name="T"/>.
        /// 
        /// Note: this method is not limited to MsAjax responses, 
        /// it will capably deserialize any valid JSON.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="serializer"></param>
        /// <param name="json"></param>
        /// <returns></returns>
        public static T CleanAndDeserialize<T>
		(this JavaScriptSerializer serializer, string json)
        {
            string innerJson = CleanWebScriptJson(json);
            return serializer.Deserialize<T>(innerJson);
        }

        /// <summary>
        /// Pre-processes <paramref name="json"/>, if necessary, 
        /// to extract the inner object from a "d:" 
        /// wrapped MsAjax response and removing "__type" properties 
        /// to allow deserialization with JavaScriptSerializer
        /// into an instance of anonymous type <typeparamref name="T"/>.
        /// 
        /// Note: this method is not limited to MsAjax responses, 
        /// it will capably deserialize any valid JSON.
        /// </summary>
        /// <typeparam name="T">The anonymous type defined by 
        /// <paramref name="anonymousPrototype"/> </typeparam>
        /// <param name="serializer"></param>
        /// <param name="json"></param>
        /// <param name="anonymousPrototype">
        /// An instance of the anonymous type into which 
        /// you would like to stuff this JSON. It simply needs to be
        /// shaped like the JSON object. 
        /// <example>
        /// string json = "{ \"name\": \"Joe\" }";
        /// var jsob = new JavaScriptSerializer().CleanAndDeserialize
        /// (json, new { name=default(string) });
        /// Debug.Assert(jsob.name=="Joe");
        /// </example>
        /// </param>
        /// <returns></returns>
        public static T CleanAndDeserialize<T>
	(this JavaScriptSerializer serializer, string json, T anonymousPrototype)
        {
            json = CleanWebScriptJson(json);
            Dictionary<string, object> dict = (Dictionary<string, 
			object>)serializer.DeserializeObject(json);
            return dict.ToAnonymousType(anonymousPrototype);
        }

        /// <summary>
        /// Extracts the inner JSON of an MS Ajax 'd' result and 
        /// removes embedded '__type' properties.
        /// </summary>
        /// <param name="json"></param>
        /// <returns>The inner JSON</returns>
        private static string CleanWebScriptJson(string json)
        {
            if (string.IsNullOrEmpty(json))
            {
                throw new ArgumentNullException("json");
            }

            Match match = RxMsAjaxJsonInner.Match(json);
            string innerJson = match.Success ? match.Groups[1].Value : json;
            return RxMsAjaxJsonInnerType.Replace(innerJson, string.Empty);
        }

        #region Dictionary to Anonymous Type

        /* An entry on Jacob Carpenter saved me from having to work this out for myself.
         * Thanks Jacob.
         * http://jacobcarpenter.wordpress.com/2008/03/13/dictionary-to-anonymous-type/
         */

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TKey"></typeparam>
        /// <typeparam name="TValue"></typeparam>
        /// <param name="dict"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        private static TValue GetValueOrDefault<TKey, TValue>
		(this IDictionary<TKey, TValue> dict, TKey key)
        {
            TValue result;
            dict.TryGetValue(key, out result);
            return result;
        }

        private static T ToAnonymousType<T, TValue>
		(this IDictionary<string, TValue> dict, T anonymousPrototype)
        {

            // get the sole constructor
            var ctor = anonymousPrototype.GetType().GetConstructors().Single();

            // conveniently named constructor parameters make this all possible...
            // TODO: sky: i think the conditional assignment could be improved
            // ReSharper disable CompareNonConstrainedGenericWithNull
            // In our typical use of this method, we are deserializing valid json, 
            // which should not contain
            // nulls for value types. So this is not a problem.
            var args = from p in ctor.GetParameters()
                       let val = dict.GetValueOrDefault(p.Name)
                       select val != null && 
			p.ParameterType.IsAssignableFrom(val.GetType()) ?
                       (object)val : null;
            // ReSharper restore CompareNonConstrainedGenericWithNull
            return (T)ctor.Invoke(args.ToArray());
        }
        #endregion
    }
}

列表 5:CleanAndDeserialize<T>() 用法

Result result = new JavaScriptSerializer().CleanAndDeserialize<Result>(responseText);

列表 6:使用匿名类型的 CleanAndDeserialize() 用法

[Test]
public void CleanAndDeserializeToAnonymousType()
{
    // To demonstrate deserializing to an anonymous type consider this:

    // This is an example of a TestClass response
    // {"d":{"__type":"TestClass:#Salient.Web.HttpLib.TestSite",
    // "Date":"\/Date(1271275580882)\/","Header":"","IntVal":99,"Name":"sky"}}

    const string responseText = 
        "{\"d\":{\"__type\":\"TestClass:#Salient.Web.HttpLib.TestSite\",\"Date\":\"\\
	/Date(1271275580882)\\/\",\"Header\":\"\",\"IntVal\":99,\"Name\":\"sky\"}}";

    // imagine for a moment that we do not have a reference to 
    // Salient.Web.HttpLib.TestSite from which
    // to deserialize this JSON into an instance of TestClass.

    // this is what an anonymous prototype of TestClass looks like

    var testClassPrototype = new
        {
            Name = default(string),
            Header = default(string),
            Date = default(DateTime),
            IntVal = default(int)
        };

    // just pass this prototype to deserialize to get a strongly typed instance 
    var jsob = new JavaScriptSerializer().CleanAndDeserialize
		(responseText, testClassPrototype);

    Assert.AreEqual("sky", jsob.Name);
    Assert.AreEqual(99, jsob.IntVal);

    // now also imagine that we are interested in only part of a JSON response,
    var prototypeOfInterestingData = new
        {
            Name = default(string)

        };

    var partialJsob = new JavaScriptSerializer().CleanAndDeserialize
			(responseText, prototypeOfInterestingData);

    Assert.AreEqual("sky", partialJsob.Name);

    // one thing to keep in mind is that anonymous types are read-only.
}

历史

  • 2010-04-15 - 添加了匿名类型支持
  • 2010-04-18 - 移除了许可限制

你可以在 http://salient.codeplex.com 找到最新的源代码和测试。

© . All rights reserved.