Связывание моделей JavaScriptSerializer и ASP.Net MVC дает разные результаты - PullRequest
8 голосов
/ 23 января 2012

Я вижу проблему десериализации JSON, которую я не могу объяснить или исправить.

код

public class Model
{
    public List<ItemModel> items { get; set; }
}
public class ItemModel
{

    public int sid { get; set; }
    public string name { get; set; }
    public DataModel data { get; set; }
    public List<ItemModel> items { get; set; }
}

public class DataModel
{
    public double? d1 { get; set; }
    public double? d2 { get; set; }
    public double? d3 { get; set; }
}

public ActionResult Save(int id, Model model) {
}

Данные

{'items':[{'sid':3157,'name':'a name','items':[{'sid':3158,'name':'child name','data':{'d1':2,'d2':null,'d3':2}}]}]}

Юнит тест - прохождение

var jss = new JavaScriptSerializer();
var m = jss.Deserialize<Model>(json);
Assert.Equal(2, m.items.First().items.First().data.d1);

Проблема

одна и та же строка JSON при отправке в действие Save не десериализуется одинаково, особенно значения D1, D2 и D3 устанавливаются в NULL. Всегда.

Что здесь происходит и как я могу это исправить?

Ответы [ 2 ]

27 голосов
/ 23 января 2012

Это может показаться нелогичным, но вы должны отправить эти двойники в виде строк в json:

'data':{'d1':'2','d2':null,'d3':'2'}

Вот мой полный тестовый код, который вызывает это действие контроллера с помощью AJAX и позволяет привязывать к каждомузначение модели:

$.ajax({
    url: '@Url.Action("save", new { id = 123 })',
    type: 'POST',
    contentType: 'application/json',
    data: JSON.stringify({
        items: [
            {
                sid: 3157,
                name: 'a name',
                items: [
                    {
                        sid: 3158,
                        name: 'child name',
                        data: {
                            d1: "2",
                            d2: null,
                            d3: "2"
                        }
                    }
                ]
            }
        ]
    }),
    success: function (result) {
        // ...
    }
});

И просто для иллюстрации масштабов проблемы десериализации числовых типов из JSON, давайте рассмотрим несколько примеров:

  • public double? Foo { get; set; }
    • { foo: 2 } => Foo = null
    • { foo: 2.0 } => Foo = null
    • { foo: 2.5 } => Foo = null
    • { foo: '2.5' } => Foo = 2.5

  • public float? Foo { get; set; }
    • { foo: 2 } => Foo = null
    • { foo: 2.0 } => Foo = null
    • { foo: 2.5 } => Foo = null
    • { foo: '2.5' } => Foo = 2,5

  • public decimal? Foo { get; set; }
    • { foo: 2 } => Foo = null
    • { foo: 2.0 } => Foo = null
    • { foo: 2.5 } => Foo= 2.5
    • { foo: '2.5' } => Foo = 2.5

Теперь давайте сделаем то же самое с ненулевыми типами:

  • public double Foo { get; set; }
    • { foo: 2 } => Foo = 2,0
    • { foo: 2.0 } => Foo = 2,0
    • { foo: 2.5 } => Foo = 2,5
    • { foo: '2.5' } => Foo = 2,5

  • public float Foo { get; set; }
    • { foo: 2 } => Foo = 2,0
    • { foo: 2.0 } => Foo = 2,0
    • { foo: 2.5 } => Foo = 2,5
    • { foo: '2.5' } => Foo = 2,5

  • public decimal Foo { get; set; }
    • { foo: 2 } => Foo = 0
    • { foo: 2.0 } => Foo = 0
    • { foo: 2.5 } => Foo = 2,5
    • { foo: '2.5' } => Foo = 2,5

Вывод: десериализация числовых типов из JSONэто один большой адский беспорядок.Используйте строки в формате JSON.И, конечно, когда вы используете строки, будьте осторожны с десятичным разделителем, так как он зависит от культуры.


В разделе комментариев меня спросили, почему это проходит модульные тесты, но не работает вASP.NET MVC.Ответ прост: это потому, что ASP.NET MVC делает намного больше вещей, чем простой вызов JavaScriptSerializer.Deserialize, что делает модульный тест.Таким образом, вы в основном сравниваете яблоки с апельсинами.

Давайте углубимся в происходящее.В ASP.NET MVC 3 есть встроенный JsonValueProviderFactory, который внутренне использует класс JavaScriptDeserializer для десериализации JSON.Это работает, как вы уже видели, в модульном тесте.Но в ASP.NET MVC есть гораздо больше, так как он также использует связыватель модели по умолчанию, который отвечает за создание экземпляров ваших параметров действий.

И если вы посмотрите на исходный код ASP.NET MVC 3,а точнее, класс DefaultModelBinder.cs, вы заметите следующий метод, который вызывается для каждого свойства, для которого будет задано значение:

public class DefaultModelBinder : IModelBinder {

    ...............

    [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "The target object should make the correct culture determination, not this method.")]
    [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")]
    private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType) {
        try {
            object convertedValue = valueProviderResult.ConvertTo(destinationType);
            return convertedValue;
        }
        catch (Exception ex) {
            modelState.AddModelError(modelStateKey, ex);
            return null;
        }
    }

    ...............

}

Давайте сосредоточимся более конкретно на следующей строке:

object convertedValue = valueProviderResult.ConvertTo(destinationType);

Если мы предположим, что у вас есть свойство типа Nullable<double>, то вот как это будет выглядеть при отладке приложения:

destinationType = typeof(double?);

Здесь никаких сюрпризов.Наш тип назначения - double?, потому что это то, что мы использовали в нашей модели представления.

Затем взглянем на valueProviderResult:

enter image description here

См. ЭтоRawValue собственность там?Можете ли вы угадать его тип?

enter image description here

Таким образом, этот метод просто генерирует исключение, потому что он явно не может преобразовать значение decimal 2.5 в double?.

Замечаете ли вы, какое значение возвращается в этом случае?Вот почему у вас в модели null.

Это очень легко проверить.Просто проверьте свойство ModelState.IsValid внутри действия вашего контроллера, и вы заметите, что оно false.И когда вы осмотрите ошибку модели, которая была добавлена ​​в состояние модели, вы увидите следующее:

Преобразование параметра из типа 'System.Decimal' в тип 'System.Nullable`1 [[System.Double, mscorlib, версия = 4.0.0.0, Culture = нейтральный, PublicKeyToken = b77a5c561934e089]] 'не удалось, поскольку преобразователь типов не может конвертировать между этими типами.

Теперь вы можете спросить: «Но почему свойство RawValue внутри ValueProviderResult типа decimal?». И снова ответ лежит в исходном коде ASP.NET MVC 3 (да, вы уже должны были его скачать). Давайте посмотрим на файл JsonValueProviderFactory.cs, а точнее на метод GetDeserializedObject:

public sealed class JsonValueProviderFactory : ValueProviderFactory {

    ............

    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();
        object jsonData = serializer.DeserializeObject(bodyText);
        return jsonData;
    }

    ............

}

Вы заметили следующую строку:

JavaScriptSerializer serializer = new JavaScriptSerializer();
object jsonData = serializer.DeserializeObject(bodyText);

Можете ли вы догадаться, что следующий фрагмент напечатает на вашей консоли?

var serializer = new JavaScriptSerializer();
var jsonData = (IDictionary<string, object>)serializer
    .DeserializeObject("{\"foo\":2.5}");
Console.WriteLine(jsonData["foo"].GetType());

Да, вы правильно догадались, это decimal.

Теперь вы можете спросить: «Но почему они использовали метод serializer.DeserializeObject вместо serializer. Как в моем модульном тесте?» Это связано с тем, что команда ASP.NET MVC приняла решение о разработке реализации привязки JSON-запроса, используя ValueProviderFactory, который не знает тип вашей модели.

Теперь посмотрите, чем ваш модульный тест полностью отличается от того, что действительно происходит под покровом ASP.NET MVC 3? Что обычно должно объяснять, почему оно проходит и почему действие контроллера не получает правильное значение модели?

0 голосов
/ 23 апреля 2013

Решение 1. Передайте данные, помеченные как «application / x-www-form-urlencoded».В этом случае Nullable<double> десериализуется правильно.Пример:

<script type="text/javascript">

$("#post-data").click(function () {
    $.ajax({
        url: "/info/create",
        type: "PUT",
        //  contentType: 'application/json', //default is application/x-www-form-urlencoded
        data: JSON.stringify({
            Dbl1: null, //pass double as null
            Dbl2: 56.3  //pass double with value
        }),
        dataType: "json"
    });

    return false;
});

Решение 2: изменить дважды?в десятичную?и отправьте контент как «application / json».Благодаря расследованию Дарина

...