zoukankan      html  css  js  c++  java
  • 基于Json.NET自己实现MVC中的JsonValueProviderFactory

    写了博文ASP.NET MVC 3升级至MVC 5.1的遭遇:“已添加了具有相同键的项”之后,继续看着System.Web.Mvc.JsonValueProviderFactory的开源代码。

    越看越不顺眼,越看心里越不爽!不爽的地方主要有两个:

    1)依然在使用用性能低下且不开源的JavaScriptSerializer!打死也不用Json.NET!

    2)作为一个工厂类,JsonValueProviderFactory实现复杂,而且工厂生产出的产品DictionaryValueProvider(IValueProvider的一个实现)也很复杂。

    【先看第一个不爽】

    JsonValueProviderFactory的工作之一是对json字符串进行反序列化,而Json.NET的反序列化性能远超JavaScriptSerializer,请看下图:

    而微软MVC开发人员依然不思进取,用自家东西的痴心不改,继续用着JavaScriptSerializer。

    private static object GetDeserializedObject(ControllerContext controllerContext)
    {
        //...
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        object jsonData = serializer.DeserializeObject(bodyText);
        return jsonData;
    }

    仅凭这一点,就让我产生了这样的冲动——基于Json.NET自己实现一个JsonValueProviderFactory。

    【再看第二个不爽】

    作为一个工厂类,JsonValueProviderFactory继承自ValueProviderFactory,重载了ValueProviderFactory的抽象方法GetValueProvider,返回接口IValueProvider的一个实现。(ControllerActionInvoker就是通过IValueProvider接口根据key得到Action各个参数的值)

    IValueProvider的代码如下:

    namespace System.Web.Mvc
    {
        public interface IValueProvider
        {
            bool ContainsPrefix(string prefix);
            ValueProviderResult GetValue(string key);
        }
    }

    接口很简单,先检查prefix是否存在,如果存在通过key取值。

    JsonValueProviderFactory返回的DictionaryValueProvider就是干这个活的,但是为了生产DictionaryValueProvider,JsonValueProviderFactory进行了复杂的搬箱子操作,不仅用到了递归,而且还用了多个IDictionary<string, object>,代码让人看得头晕。

    再看看DictionaryValueProvider的实现,也是复杂,而且还用到了PrefixContainer。

    简单算个账:JsonValueProviderFactory的代码用了120行,DictionaryValueProvider的代码用了63行,PrefixContainer的代码用了219,一共用了402行代码(包含空行与命名空间的引用)。有些奢侈!

    需要这么复杂吗?有更简单的解决方法吗?

    【冲动不如行动】

    解决问题的关键在于如何以更简单的方法实现IValueProvider的两个操作——ContainsPrefix与GetValue。

    要实现这两个操作,先要摸清prefix与key的规律。于是先实现一个MockValueProvider,通过日志记录ControllerActionInvoker调用这个接口时使用的参数。

    通过日志信息,找出了这样的规律:

    1. 如果Action的参数是这样的:

    public ActionResult PostList(AggSiteModel model)
    {
    }

    ControllerActionInvoker会这样调用:

    ContainsPrefix("model") -> 如果返回False -> 以AggSiteModel的属性名称依次ContainsPrefix,比如ContainsPrefix("PageTitle")【注:PageTitle是AggSiteModel的一个属性】 -> 如果返回True -> 会以prefix为key调用GetValue。

    2. 如果Action的参数是数组:

    public ActionResult PostList(AggSiteModel[] model)
    {
    }

    ControllerActionInvoker会这样调用:

    ContainsPrefix("[0]") -> 如果返回True -> ContainsPrefix("[0].PageTitle") -> 如果返回True -> GetValue("[0].PageTitle")

    3. 依然是第1种的Action参数形式,只不过AggSiteModel有聚合。

    public class AggSiteModel
    {
        public string PageTitle { get; set; } 
        public PagingBuilder Paging { get; set; }
    }

    ControllerActionInvoker会这样调用:

    ContainsPrefix("model") -> 如果返回False -> ContainsPrefix("Paging.PageTitle")

    看到这些key的特征,想到了Json.NET中的SelectTokens:

    /// <summary>
    /// Selects a collection of elements using a JPath expression.
    /// </summary>
    /// <param name="path">
    /// A <see cref="String"/> that contains a JPath expression.
    /// </param>
    /// <returns>An <see cref="IEnumerable{JToken}"/> that contains the selected elements.</returns>
    public IEnumerable<JToken> SelectTokens(string path)
    {
        return SelectTokens(path, false);
    }

    这是里key竟然与JPath惊人的相似!

    看来Json.NET不仅可以搞定JsonValueProviderFactory,还可以搞定DictionaryValueProvider+PrefixContainer,实现代码应该不会超过100行。

    【基于Json.NET实现CnblogsJsonValueProviderFactory】

    public class CnblogsJsonValueProviderFactory : ValueProviderFactory
    {
        public override IValueProvider GetValueProvider(ControllerContext controllerContext)
        {
            if (controllerContext == null) throw new ArgumentNullException("controllerContext");            
    
            if (!controllerContext.HttpContext.Request.ContentType.
                StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
            {
                return null;
            }
    
            var bodyText = string.Empty;
            using (var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream))
            {
                bodyText = reader.ReadToEnd();
            }
            if (string.IsNullOrEmpty(bodyText)) return null;
    
            return new JObjectValueProvider(bodyText.StartsWith("[") ? 
                JArray.Parse(bodyText) as JContainer :
                JObject.Parse(bodyText) as JContainer);
        }
    }
    
    public class JObjectValueProvider : IValueProvider
    {
        private JContainer _jcontainer;
    
        public JObjectValueProvider(JContainer jcontainer)
        {
            _jcontainer = jcontainer;
        }
    
        public bool ContainsPrefix(string prefix)
        {
            return _jcontainer.SelectToken(prefix) != null;
        }
    
        public ValueProviderResult GetValue(string key)
        {
            var jtoken = _jcontainer.SelectToken(key);
            if (jtoken == null || jtoken.Type == JTokenType.Object) return null;
            return new ValueProviderResult(jtoken.ToObject<object>(), jtoken.ToString(), CultureInfo.CurrentCulture);
        }
    }

    包含空行与命名空间的引用,一共只有61行代码,远远少于MVC中的402行代码。

    在项目中使用这个CnblogsJsonValueProviderFactory:

    protected void Application_Start(Object sender, EventArgs e)
    {
        ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.
            OfType<JsonValueProviderFactory>().FirstOrDefault());
        ValueProviderFactories.Factories.Add(new CnblogsJsonValueProviderFactory());
    }

    【美中不足】

    Json.NET中的SelectTokens的path参数区分大小写,使用CnblogsJsonValueProviderFactory,在js中写json时,大小写必须要匹配。 

    看了一下Json.NET的开源代码,发现是与下面的代码有关:

    internal class JPropertyKeyedCollection : Collection<JToken>
    {
        private static readonly IEqualityComparer<string> Comparer = StringComparer.Ordinal;
    }

    如果把StringComparer.Ordinal改为StringComparer.OrdinalIgnoreCase就能解决问题,但是不知道会不会给Json.NET的性能带来影响。

    严格区分大小写也能接受,可以让代码更规范一些。

  • 相关阅读:
    对象的继承关系在数据库中的实现方式和PowerDesigner设计
    Oracle数据库需要修改默认的Profiles,避免用户密码过期
    如何将数据库从SQL Server迁移到MySQL
    NHibernate中对同一个对象的Lazyload要设置一致
    时来运转乎
    如何用VS里的部署实现在Duwamish7安装时的自动创建数据功能
    Windows 2003里的一个小bug?
    DailyBuild全攻略"隆重"发布V1.0
    建议DuDu:实现上传图片能够以目录方式存放.
    如何在VS里的部署中执行一段 .sql 的脚本文件?
  • 原文地址:https://www.cnblogs.com/dudu/p/mvc_json_value_provider_factory.html
Copyright © 2011-2022 走看看