web-dev-qa-db-fra.com

Exception JsonMaxLength lors de la désérialisation de gros objets JSON

Intro:

Application Web, ASP.NET MVC 3, une action de contrôleur qui accepte une instance de classe de modèle POCO avec un champ (potentiellement) grand.

Classe de modèle:

public class View
{
    [Required]
    [RegularExpression(...)]
    public object name { get; set; }
    public object details { get; set; }
    public object content { get; set; } // the problem field
}

Action du contrôleur:

[ActionName(...)]
[Authorize(...)]
[HttpPost]
public ActionResult CreateView(View view)
{
    if (!ModelState.IsValid) { return /*some ActionResult here*/;}
    ... //do other stuff, create object in db etc. return valid result
}

Problème:

Une action doit pouvoir accepter des objets JSON volumineux (au moins jusqu'à cent mégaoctets en une seule demande et ce n'est pas une blague). Par défaut, j'ai rencontré plusieurs restrictions comme httpRuntime maxRequestLength etc. - tous résolus sauf MaxJsonLengh - ce qui signifie que ValueProviderFactory par défaut pour JSON n'est pas capable de gérer de tels objets.

Testé:

Réglage

  <system.web.extensions>
    <scripting>
      <webServices>
        <jsonSerialization maxJsonLength="2147483647"/>
      </webServices>
    </scripting>
  </system.web.extensions>
  • n'aide pas.

Créer mon propre ValueProviderFactory personnalisé comme décrit dans la réponse de @ Darin ici:

JsonValueProviderFactory lève "demande trop grande"

  • a également échoué car je n'ai pas la possibilité d'utiliser JSON.Net (pour des raisons non techniques). J'ai essayé d'implémenter la désérialisation correcte ici, mais apparemment, c'est un peu au-dessus de mes connaissances (pour l'instant). J'ai pu désérialiser ma chaîne JSON en Dictionary<String,Object> ici, mais ce n'est pas ce que je veux - je veux le désérialiser à mes beaux objets POCO et les utiliser comme paramètres d'entrée pour les actions.

Donc, les questions:

  1. N'importe qui sait mieux pour résoudre le problème sans implémenter la valeur universelle personnalisée ValueProviderFactory?
  2. Existe-t-il une possibilité de spécifier pour quel contrôleur et quelle action spécifiques je souhaite utiliser mon ValueProviderFactory personnalisé? Si je connais l'action à l'avance, je pourrai désérialiser JSON en POCO sans beaucoup de codage dans ValueProviderFactory ...
  3. Je pense également à l'implémentation d'un ActionFilter personnalisé pour ce problème spécifique, mais je pense que c'est un peu moche.

N'importe qui peut suggérer une bonne solution?

28
Sergey Kudriavtsev

Le JsonValueProviderFactory intégré ignore le <jsonSerialization maxJsonLength="50000000"/> réglage. Vous pouvez donc écrire une fabrique personnalisée en utilisant l'implémentation intégrée:

public sealed class MyJsonValueProviderFactory : ValueProviderFactory
{
    private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
    {
        IDictionary<string, object> d = value as IDictionary<string, object>;
        if (d != null)
        {
            foreach (KeyValuePair<string, object> 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 object GetDeserializedObject(ControllerContext controllerContext)
    {
        if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
        {
            // not JSON request
            return null;
        }

        StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
        string bodyText = reader.ReadToEnd();
        if (String.IsNullOrEmpty(bodyText))
        {
            // no JSON data
            return null;
        }

        JavaScriptSerializer serializer = new JavaScriptSerializer();
        serializer.MaxJsonLength = 2147483647;
        object jsonData = serializer.DeserializeObject(bodyText);
        return jsonData;
    }

    public override IValueProvider GetValueProvider(ControllerContext controllerContext)
    {
        if (controllerContext == null)
        {
            throw new ArgumentNullException("controllerContext");
        }

        object jsonData = GetDeserializedObject(controllerContext);
        if (jsonData == null)
        {
            return null;
        }

        Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        AddToBackingStore(backingStore, String.Empty, jsonData);
        return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
    }

    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;
    }
}

La seule modification que j'ai faite par rapport à l'usine par défaut est l'ajout de la ligne suivante:

serializer.MaxJsonLength = 2147483647;

Malheureusement cette usine n'est pas du tout extensible, des trucs scellés donc j'ai dû la recréer.

et dans votre Application_Start:

ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<System.Web.Mvc.JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new MyJsonValueProviderFactory());
66
Darin Dimitrov

J'ai trouvé que le maxRequestLength n'a pas résolu le problème cependant. J'ai résolu mon problème avec le paramètre ci-dessous. C'est plus propre que d'avoir à implémenter une valeur ValueProviderFactory

<appSettings>
  <add key="aspnet:MaxJsonDeserializerMembers" value="150000" />
</appSettings>

Le mérite revient aux questions suivantes:

JsonValueProviderFactory lève "demande trop grande"

Obtenir "La requête JSON était trop volumineuse pour être désérialisée"

Ce paramètre concerne évidemment un modèle json très complexe et non la taille réelle.

17
Oliver

La solution de Darin Dimitrov fonctionne pour moi mais j'ai besoin de réinitialiser la position du flux de la demande avant de la lire, en ajoutant cette ligne:

controllerContext.HttpContext.Request.InputStream.Position = 0;

Alors maintenant, la méthode GetDeserializedObject ressemble à ceci:

 private static object GetDeserializedObject(ControllerContext controllerContext)
    {
        if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
        {
            // not JSON request
            return null;
        }
        controllerContext.HttpContext.Request.InputStream.Position = 0;
        StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
        string bodyText = reader.ReadToEnd();
        if (String.IsNullOrEmpty(bodyText))
        {
            // no JSON data
            return null;
        }

        JavaScriptSerializer serializer = new JavaScriptSerializer();
        serializer.MaxJsonLength = 2147483647;
        object jsonData = serializer.DeserializeObject(bodyText);
        return jsonData;
    }
4