diff --git a/src/ServiceStack.Text/AutoMappingUtils.cs b/src/ServiceStack.Text/AutoMappingUtils.cs index 471155d2f..423fd4e1a 100644 --- a/src/ServiceStack.Text/AutoMappingUtils.cs +++ b/src/ServiceStack.Text/AutoMappingUtils.cs @@ -810,23 +810,19 @@ public static GetMemberDelegate CreateTypeConverter(Type fromType, Type toType) if (underlyingToType.IsIntegerType()) return fromValue => Convert.ChangeType(fromValue, underlyingToType, null); } - else if (toType.IsNullableType()) - { - return null; - } else if (typeof(IEnumerable).IsAssignableFrom(fromType)) { return fromValue => { var listResult = TranslateListWithElements.TryTranslateCollections( - fromType, toType, fromValue); + fromType, underlyingToType, fromValue); return listResult ?? fromValue; }; } - else if (toType.IsValueType) + else if (underlyingToType.IsValueType) { - return fromValue => Convert.ChangeType(fromValue, toType, provider: null); + return fromValue => Convert.ChangeType(fromValue, underlyingToType, provider: null); } else { diff --git a/tests/ServiceStack.Text.Tests/AutoMappingObjectDictionaryTests.cs b/tests/ServiceStack.Text.Tests/AutoMappingObjectDictionaryTests.cs index 2cbf9051d..07948167a 100644 --- a/tests/ServiceStack.Text.Tests/AutoMappingObjectDictionaryTests.cs +++ b/tests/ServiceStack.Text.Tests/AutoMappingObjectDictionaryTests.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using Northwind.Common.DataModel; using NUnit.Framework; +using ServiceStack.Common.Tests.Models; namespace ServiceStack.Text.Tests { @@ -162,6 +164,163 @@ Dictionary MergeObjects(params object[] sources) { Assert.That(employee.DisplayName, Is.EqualTo("John Z Doe")); } + [Test, TestCaseSource(nameof(TestDataFromObjectDictionaryWithNullableTypes))] + public void Can_Convert_from_ObjectDictionary_with_Nullable_Properties( + Dictionary map, + ModelWithFieldsOfNullableTypes expected) + { + var actual = map.FromObjectDictionary(); + + ModelWithFieldsOfNullableTypes.AssertIsEqual(actual, expected); + } + + private static IEnumerable TestDataFromObjectDictionaryWithNullableTypes + { + get + { + var defaults = ModelWithFieldsOfNullableTypes.CreateConstant(1); + + yield return new TestCaseData( + new Dictionary + { + { "Id", defaults.Id }, + { "NId", defaults.NId }, + { "NLongId", defaults.NLongId }, + { "NGuid", defaults.NGuid }, + { "NBool", defaults.NBool }, + { "NDateTime", defaults.NDateTime }, + { "NFloat", defaults.NFloat }, + { "NDouble", defaults.NDouble }, + { "NDecimal", defaults.NDecimal }, + { "NTimeSpan", defaults.NTimeSpan } + }, + defaults).SetName("All values populated"); + + yield return new TestCaseData( + new Dictionary + { + { "Id", defaults.Id.ToString() }, + { "NId", defaults.NId.ToString() }, + { "NLongId", defaults.NLongId.ToString() }, + { "NGuid", defaults.NGuid.ToString() }, + { "NBool", defaults.NBool.ToString() }, + { "NDateTime", defaults.NDateTime?.ToString("o") }, + { "NFloat", defaults.NFloat.ToString() }, + { "NDouble", defaults.NDouble.ToString() }, + { "NDecimal", defaults.NDecimal.ToString() }, + { "NTimeSpan", defaults.NTimeSpan.ToString() } + }, + defaults).SetName("All values populated as strings"); + + yield return new TestCaseData( + new Dictionary + { + { "Id", defaults.Id }, + { "NId", null }, + { "NLongId", null }, + { "NGuid", null }, + { "NBool", null }, + { "NDateTime", null }, + { "NFloat", null }, + { "NDouble", null }, + { "NDecimal", null }, + { "NTimeSpan", null } + }, + new ModelWithFieldsOfNullableTypes + { + Id = defaults.Id + }).SetName("Nullables set to null"); + + yield return new TestCaseData( + new Dictionary + { + { "Id", defaults.Id } + }, + new ModelWithFieldsOfNullableTypes + { + Id = defaults.Id + }).SetName("Nullables unassigned"); + + yield return new TestCaseData( + new Dictionary + { + { "Id", defaults.Id }, + { "NLongId", 2 }, + { "NFloat", "3.1" }, + { "NDecimal", 4.2d }, + { "NTimeSpan", null } + }, + new ModelWithFieldsOfNullableTypes + { + Id = defaults.Id, + NLongId = 2, + NFloat = 3.1f, + NDecimal = 4.2m + }).SetName("Mixed properties"); + + yield return new TestCaseData( + new Dictionary + { + { "Id", defaults.Id }, + { "NMadeUp", 99.9 }, + { "NLongId", 2 }, + { "NFloat", "3.1" }, + { "NRandom", "RANDOM" }, + { "NDecimal", 4.2d }, + { "NTimeSpan", null }, + { "NNull", null } + }, + new ModelWithFieldsOfNullableTypes + { + Id = defaults.Id, + NLongId = 2, + NFloat = 3.1f, + NDecimal = 4.2m + }).SetName("Mixed properties with some foreign key/values"); + } + } + + [Test] + public void Can_Convert_from_ObjectDictionary_with_Nullable_Collection_Properties() + { + var map = new Dictionary + { + { "Id", 1 }, + { "Users", new[] { new User { FirstName = "Foo", LastName = "Bar", Car = new Car { Name = "Jag", Age = 25 }}}}, + { "Cars", new List { new Car { Name = "Toyota", Age = 2 }, new Car { Name = "Lexus", Age = 1 }}}, + { "Colors", null } + }; + + var actual = map.FromObjectDictionary(); + + Assert.That(actual.Id, Is.EqualTo(1)); + Assert.That(actual.Users, Is.Not.Null); + Assert.That(actual.Users.Count(), Is.EqualTo(1)); + var user = actual.Users.Single(); + Assert.That(user.FirstName, Is.EqualTo("Foo")); + Assert.That(user.LastName, Is.EqualTo("Bar")); + Assert.That(user.Car, Is.Not.Null); + Assert.That(user.Car.Name, Is.EqualTo("Jag")); + Assert.That(user.Car.Age, Is.EqualTo(25)); + Assert.That(actual.Cars, Is.Not.Null); + Assert.That(actual.Cars.Count, Is.EqualTo(2)); + var firstCar = actual.Cars.First(); + Assert.That(firstCar.Name, Is.EqualTo("Toyota")); + Assert.That(firstCar.Age, Is.EqualTo(2)); + var secondCar = actual.Cars.Last(); + Assert.That(secondCar.Name, Is.EqualTo("Lexus")); + Assert.That(secondCar.Age, Is.EqualTo(1)); + Assert.That(actual.Colors, Is.Null); + } + + public class ModelWithCollectionsOfNullableTypes + { + public int Id { get; set; } + public IEnumerable Users { get; set; } + public Car[] Cars { get; set; } + public IList Colors { get; set; } + } } + } \ No newline at end of file