基於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的性能帶來影響。

嚴格區分大小寫也能接受,可以讓代碼更規范一些。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM