Это может показаться нелогичным, но вы должны отправить эти двойники в виде строк в 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
:
См. ЭтоRawValue
собственность там?Можете ли вы угадать его тип?
Таким образом, этот метод просто генерирует исключение, потому что он явно не может преобразовать значение 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? Что обычно должно объяснять, почему оно проходит и почему действие контроллера не получает правильное значение модели?