基于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的性能带来影响。

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

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。