Unexpected behavior in ConvertTo() following 6.3.0 upgrade

We recently upgraded from ServiceStack 5.10.4 to 6.3.0 and have observed behavior changes when using the ConvertTo and PopulateWith methods. In general, we are running into issues related to nullable value types and default values being set based on the underlying types rather than a null value being used.

The examples below should illustrate what we found.

Type Definitions

internal class Source
{
    public int NonNullableId { get; set; }
    public int? NullableIdNonMatchingUnderlyingType { get; set; }
    public int? NullableIdMatchingUnderlyingType { get; set; }
    public DateTime? DateTime { get; set; }
}
internal class Target
{
    public int NonNullableId { get; set; }
    public long? NullableIdNonMatchingUnderlyingType { get; set; }
    public int? NullableIdMatchingUnderlyingType { get; set; }
    public DateTime? NullableDateTime { get; set; }
    public DateTime DateTime { get; set; }
    public DateTime TargetOnlyDateTime { get; set; }
}

Scenario 1 - ConvertTo

Source source = new Source();
Target target = source.ConvertTo<Target>();
Source Data Dump:
{
	NonNullableId: 0
}

Target Data Dump:
{
	NonNullableId: 0,
	NullableIdNonMatchingUnderlyingType: 0,
	DateTime: 0001-01-01,
	TargetOnlyDateTime: 0001-01-01
}

Behavior:
NullableIdNonMatchingUnderlyingType - Converting a null from the int? to long? results in a default value, 0, on the target, long, rather than a null. A default for long? would also be fine as that is null.

Scenario 2 - PopulateWith (Existing Values)

Source source = new Source();
Target target = new Target
{
    NonNullableId = 4,
    NullableIdNonMatchingUnderlyingType = 3,
    DateTime = DateTime.UtcNow,
    TargetOnlyDateTime = DateTime.UtcNow
};
target.PopulateWith(source);
Target Data Dump Before PopulateWith:
{
	NonNullableId: 4,
	NullableIdNonMatchingUnderlyingType: 3,
	DateTime: 2023-05-25T20:24:40.6815367Z,
	TargetOnlyDateTime: 2023-05-25T20:24:40.6815367Z
}

Target Data Dump After PopulateWith:
{
	NonNullableId: 0,
	NullableIdNonMatchingUnderlyingType: 0,
	DateTime: 0001-01-01,
	TargetOnlyDateTime: 2023-05-25T20:24:40.6815367Z
}

Behavior:
Existing values are getting overwritten for both nullable and non-nullable values. Default values for types/underlying types are getting set on the target.

Scenario 3 - PopulateWithNonDefaultValues (Existing Values)
We ran this scenario to compare behavior.

Source source = new Source();
Target target = new Target
{
    NonNullableId = 4,
    NullableIdNonMatchingUnderlyingType = 3,
    DateTime = DateTime.UtcNow,
    TargetOnlyDateTime = DateTime.UtcNow
};
target.PopulateWithNonDefaultValues(source);
Target Data Dump Before PopulateWithNonDefaultValues:
{
	NonNullableId: 4,
	NullableIdNonMatchingUnderlyingType: 3,
	DateTime: 2023-05-25T20:24:40.6815367Z,
	TargetOnlyDateTime: 2023-05-25T20:24:40.6815367Z
}

Target Data Dump After PopulateWithNonDefaultValues:
{
	NonNullableId: 4,
	NullableIdNonMatchingUnderlyingType: 0,
	DateTime: 0001-01-01,
	TargetOnlyDateTime: 2023-05-25T20:24:40.6815367Z
}

Behavior:
As you can see, we now have a mix of behavior where some values are preserved, whereas others are not.

While I’m not crazy about the solution, I am able to resolve the first scenario by adding the following converter:

AutoMapping.RegisterConverter<int?, long?>(i =>
{
    if (i == null)
    {
        return null;
    }
    return i.Value;
});

We are hoping to get some guidance in determining:

  • If we are running into bugs.
  • If we need additional configuration to achieve what we are expecting.
  • If we need to change our code due to previous behavior expectations now being incorrect.

Yeah the expected behavior should preserve null’s between nullables of different types, resolved in this commit.

This change is available from v6.8.1+ that’s now available on MyGet.

Wow! Thanks so much for the speedy response and fix!

1 Like