Skip to content

Commit 462c798

Browse files
committed
Serializers and model binders for keyed value objects check whether the (struct) value is missing, everything else is the responsibility of the validation
1 parent 4f73321 commit 462c798

File tree

8 files changed

+66
-58
lines changed

8 files changed

+66
-58
lines changed

src/Thinktecture.Runtime.Extensions.AspNetCore/AspNetCore/ModelBinding/ThinktectureModelBinderBase.cs

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ public abstract class ThinktectureModelBinderBase<T, TKey, TValidationError> : I
1818
{
1919
private static readonly Type _type = typeof(T);
2020
private static readonly Type _keyType = typeof(TKey);
21-
private static readonly TKey? _keyDefaultValue = default;
2221
private static readonly bool _mayReturnInvalidObjects = typeof(IValidatableEnum).IsAssignableFrom(typeof(T));
2322
private static readonly bool _disallowDefaultValues = typeof(IDisallowDefaultValue).IsAssignableFrom(typeof(T));
2423

@@ -99,7 +98,17 @@ public Task BindModelAsync(ModelBindingContext bindingContext)
9998
return Task.CompletedTask;
10099
}
101100

102-
CheckKey(bindingContext, valueProviderResult, (TKey)key);
101+
var validationError = T.Validate((TKey)key, valueProviderResult.Culture, out var obj);
102+
103+
if (validationError is null || _mayReturnInvalidObjects)
104+
{
105+
bindingContext.ModelState.MarkFieldValid(bindingContext.ModelName);
106+
bindingContext.Result = ModelBindingResult.Success(obj);
107+
}
108+
else
109+
{
110+
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, validationError.ToString() ?? $"There is no item of type '{typeof(T).Name}' with the identifier '{key}'.");
111+
}
103112

104113
return Task.CompletedTask;
105114
}
@@ -122,24 +131,4 @@ public Task BindModelAsync(ModelBindingContext bindingContext)
122131
return Task.CompletedTask;
123132
}
124133
}
125-
126-
private static void CheckKey(ModelBindingContext bindingContext, ValueProviderResult valueProviderResult, TKey key)
127-
{
128-
if (_disallowDefaultValues && key.Equals(_keyDefaultValue))
129-
{
130-
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, $"Cannot convert the value {_keyDefaultValue} to type \"{typeof(T).Name}\" because it doesn't allow default values.");
131-
return;
132-
}
133-
134-
var validationError = T.Validate(key, valueProviderResult.Culture, out var obj);
135-
136-
if (validationError is null || _mayReturnInvalidObjects)
137-
{
138-
bindingContext.ModelState.MarkFieldValid(bindingContext.ModelName);
139-
bindingContext.Result = ModelBindingResult.Success(obj);
140-
return;
141-
}
142-
143-
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, validationError.ToString() ?? $"There is no item of type '{typeof(T).Name}' with the identifier '{key}'.");
144-
}
145134
}

src/Thinktecture.Runtime.Extensions.Json/Text/Json/Serialization/ThinktectureJsonConverter.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ public class ThinktectureJsonConverter<T, TKey, TValidationError> : JsonConverte
6060
{
6161
private static readonly bool _mayReturnInvalidObjects = typeof(IValidatableEnum).IsAssignableFrom(typeof(T));
6262
private static readonly bool _disallowDefaultValues = typeof(IDisallowDefaultValue).IsAssignableFrom(typeof(T));
63-
private static readonly TKey? _keyDefaultValue = default;
6463

6564
private readonly JsonConverter<TKey> _keyConverter;
6665

@@ -88,9 +87,6 @@ public ThinktectureJsonConverter(JsonSerializerOptions options)
8887
return default;
8988
}
9089

91-
if (_disallowDefaultValues && key.Equals(_keyDefaultValue))
92-
throw new JsonException($"Cannot convert the value {_keyDefaultValue} to type \"{typeof(T).Name}\" because it doesn't allow default values.");
93-
9490
var validationError = T.Validate(key, null, out var obj);
9591

9692
if (validationError is not null && !_mayReturnInvalidObjects)

src/Thinktecture.Runtime.Extensions.MessagePack/Formatters/ThinktectureStructMessagePackFormatter.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ public class ThinktectureStructMessagePackFormatter<T, TKey, TValidationError> :
3131
{
3232
private static readonly bool _mayReturnInvalidObjects = typeof(IValidatableEnum).IsAssignableFrom(typeof(T));
3333
private static readonly bool _disallowDefaultValues = typeof(IDisallowDefaultValue).IsAssignableFrom(typeof(T));
34-
private static readonly TKey? _keyDefaultValue = default;
3534

3635
/// <inheritdoc />
3736
public void Serialize(ref MessagePackWriter writer, T value, MessagePackSerializerOptions options)
@@ -67,9 +66,6 @@ public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions
6766
return default;
6867
}
6968

70-
if (_disallowDefaultValues && key.Equals(_keyDefaultValue))
71-
throw new MessagePackSerializationException($"Cannot convert the value {_keyDefaultValue} to type \"{typeof(T).Name}\" because it doesn't allow default values.");
72-
7369
return Deserialize(key);
7470
}
7571

src/Thinktecture.Runtime.Extensions.Newtonsoft.Json/Json/ThinktectureNewtonsoftJsonConverter.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public class ThinktectureNewtonsoftJsonConverter<T, TKey, TValidationError> : Js
2727
where TValidationError : class, IValidationError<TValidationError>
2828
{
2929
private static readonly Type _type = typeof(T);
30-
private static readonly TKey? _keyDefaultValue = default;
3130
private static readonly bool _mayReturnInvalidObjects = typeof(IValidatableEnum).IsAssignableFrom(typeof(T));
3231
private static readonly bool _disallowDefaultValues = typeof(IDisallowDefaultValue).IsAssignableFrom(typeof(T));
3332

@@ -77,9 +76,6 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer
7776
return null;
7877
}
7978

80-
if (_disallowDefaultValues && key.Equals(_keyDefaultValue))
81-
throw new JsonException($"Cannot convert the value {_keyDefaultValue} to type \"{typeof(T).Name}\" because it doesn't allow default values.");
82-
8379
var validationError = T.Validate(key, null, out var obj);
8480

8581
if (validationError is not null && !_mayReturnInvalidObjects)

test/Thinktecture.Runtime.Extensions.AspNetCore.Tests/AspNetCore/ModelBinding/ThinktectureModelBinderTests/BindModelAsync.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ public void Should_try_bind_keyed_value_object_when_value_is_null_or_default()
8080
FluentActions.Invoking(() => Bind<IntBasedStructValueObject>(null))
8181
.Should().Throw<Exception>().WithMessage("Cannot convert null to type \"IntBasedStructValueObject\".");
8282

83-
FluentActions.Invoking(() => Bind<IntBasedStructValueObjectDoesNotAllowDefaultStructs>("0")) // AllowDefaultStructs = true
84-
.Should().Throw<Exception>().WithMessage("Cannot convert the value 0 to type \"IntBasedStructValueObjectDoesNotAllowDefaultStructs\" because it doesn't allow default values.");
83+
Bind<IntBasedStructValueObjectDoesNotAllowDefaultStructs>("0").Should().Be(IntBasedStructValueObjectDoesNotAllowDefaultStructs.Create(0)); // AllowDefaultStructs = true
8584

8685
// nullable struct - string
8786
Bind<StringBasedStructValueObject?>(null).Should().Be(null);

test/Thinktecture.Runtime.Extensions.Json.Tests/Text/Json/Serialization/ThinktectureJsonConverterFactoryTests/ReadJson.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@ public void Should_try_deserialize_keyed_value_object_when_value_is_null_or_defa
8383
FluentActions.Invoking(() => Deserialize<IntBasedStructValueObject>("null"))
8484
.Should().Throw<JsonException>().WithMessage("The JSON value could not be converted to Thinktecture.Runtime.Tests.TestValueObjects.IntBasedStructValueObject. Path: $ | LineNumber: 0 | BytePositionInLine: 4.");
8585

86-
FluentActions.Invoking(() => Deserialize<IntBasedStructValueObjectDoesNotAllowDefaultStructs>("0")) // AllowDefaultStructs = true
87-
.Should().Throw<JsonException>().WithMessage("Cannot convert the value 0 to type \"IntBasedStructValueObjectDoesNotAllowDefaultStructs\" because it doesn't allow default values.");
86+
Deserialize<IntBasedStructValueObjectDoesNotAllowDefaultStructs>("0").Should().Be(IntBasedStructValueObjectDoesNotAllowDefaultStructs.Create(0)); // AllowDefaultStructs = true
8887

8988
// nullable struct - string
9089
Deserialize<StringBasedStructValueObject?>("null").Should().Be(null);
@@ -99,11 +98,15 @@ public void Should_try_deserialize_keyed_value_object_when_value_is_null_or_defa
9998
}
10099

101100
[Fact]
102-
public void Should_throw_if_AllowDefaultStructs_is_disabled_on_keyed_value_object_and_value_is_null_and_default()
101+
public void Should_not_throw_if_AllowDefaultStructs_is_disabled_on_keyed_value_object_and_value_is_default()
103102
{
104103
FluentActions.Invoking(() => Deserialize<GenericClass<IntBasedStructValueObjectDoesNotAllowDefaultStructs>>("{\"Property\": 0 }"))
105-
.Should().Throw<JsonException>().WithMessage("Cannot convert the value 0 to type \"IntBasedStructValueObjectDoesNotAllowDefaultStructs\" because it doesn't allow default values.");
104+
.Should().NotThrow();
105+
}
106106

107+
[Fact]
108+
public void Should_throw_if_AllowDefaultStructs_is_disabled_on_keyed_value_object_and_value_is_null()
109+
{
107110
FluentActions.Invoking(() => Deserialize<GenericClass<StringBasedStructValueObject>>("{\"Property\": null }"))
108111
.Should().Throw<JsonException>().WithMessage("Cannot convert null to type \"StringBasedStructValueObject\" because it doesn't allow default values.");
109112
}
@@ -308,15 +311,20 @@ public void Should_throw_if_AllowDefaultStructs_is_disabled_on_complex_value_obj
308311
}
309312

310313
[Fact]
311-
public void Should_throw_if_complex_value_object_property_has_AllowDefaultStructs_equals_to_false_and_value_is_null_or_default()
314+
public void Should_not_throw_if_complex_value_object_property_has_AllowDefaultStructs_equals_to_false_and_value_is_default()
312315
{
313-
// property is null or default
316+
// property is default
314317
FluentActions.Invoking(() => Deserialize<ComplexValueObjectDoesNotAllowDefaultStructsWithInt>("{ \"Property\": 0 }"))
315318
.Should().NotThrow();
316319

317320
FluentActions.Invoking(() => Deserialize<ComplexValueObjectDoesNotAllowDefaultStructsWithIntBasedStruct>("{ \"Property\": 0 }"))
318-
.Should().Throw<JsonException>().WithMessage("Cannot convert the value 0 to type \"IntBasedStructValueObjectDoesNotAllowDefaultStructs\" because it doesn't allow default values.");
321+
.Should().NotThrow();
322+
}
319323

324+
[Fact]
325+
public void Should_throw_if_complex_value_object_property_has_AllowDefaultStructs_equals_to_false_and_value_is_null_or_default()
326+
{
327+
// property is null
320328
FluentActions.Invoking(() => Deserialize<ComplexValueObjectDoesNotAllowDefaultStructsWithStringBasedStruct>("{ \"Property\": null }"))
321329
.Should().Throw<JsonException>().WithMessage("Cannot convert null to type \"StringBasedStructValueObject\" because it doesn't allow default values.");
322330

test/Thinktecture.Runtime.Extensions.MessagePack.Tests/Formatters/ThinktectureMessagePackFormatterTests/RoundtripSerialize.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace Thinktecture.Runtime.Tests.Formatters.ThinktectureMessagePackFormatter
1515
public partial class RoundTripSerialize
1616
{
1717
private static readonly MethodInfo _serializeRoundTripMethodInfo = typeof(RoundTripSerialize).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
18-
.SingleOrDefault(m => m.Name == nameof(RoundTrip) && m.GetParameters().Length == 1)
18+
.SingleOrDefault(m => m.Name == nameof(RoundTrip) && m.GetParameters().Length == 1)
1919
?? throw new Exception($"Method '{nameof(RoundTrip)}' not found.");
2020

2121
private readonly MessagePackSerializerOptions _options;
@@ -94,9 +94,7 @@ public void Should_try_deserialize_keyed_value_object_when_value_is_null_or_defa
9494
.Should().Throw<MessagePackSerializationException>()
9595
.WithInnerException<MessagePackSerializationException>().WithMessage("Unexpected msgpack code 192 (nil) encountered.");
9696

97-
FluentActions.Invoking(() => RoundTrip(0, IntBasedStructValueObjectDoesNotAllowDefaultStructs.Create(-1))) // AllowDefaultStructs = true
98-
.Should().Throw<MessagePackSerializationException>()
99-
.WithInnerException<MessagePackSerializationException>().WithMessage("Cannot convert the value 0 to type \"IntBasedStructValueObjectDoesNotAllowDefaultStructs\" because it doesn't allow default values.");
97+
FluentActions.Invoking(() => RoundTrip(0, IntBasedStructValueObjectDoesNotAllowDefaultStructs.Create(0))); // AllowDefaultStructs = true
10098

10199
// nullable struct - string
102100
RoundTrip((object)null, (StringBasedStructValueObject?)null);
@@ -113,12 +111,15 @@ public void Should_try_deserialize_keyed_value_object_when_value_is_null_or_defa
113111
}
114112

115113
[Fact]
116-
public void Should_throw_if_AllowDefaultStructs_is_disabled_on_keyed_value_object_and_value_is_null_and_default()
114+
public void Should_not_throw_if_AllowDefaultStructs_is_disabled_on_keyed_value_object_and_value_is_default()
117115
{
118-
FluentActions.Invoking(() => RoundTrip(new GenericClass<int>(0), (GenericClass<IntBasedStructValueObjectDoesNotAllowDefaultStructs>)null))
119-
.Should().Throw<MessagePackSerializationException>()
120-
.WithInnerException<MessagePackSerializationException>().WithMessage("Cannot convert the value 0 to type \"IntBasedStructValueObjectDoesNotAllowDefaultStructs\" because it doesn't allow default values.");
116+
FluentActions.Invoking(() => RoundTrip(new GenericClass<int>(0), new GenericClass<IntBasedStructValueObjectDoesNotAllowDefaultStructs>(IntBasedStructValueObjectDoesNotAllowDefaultStructs.Create(0))))
117+
.Should().NotThrow();
118+
}
121119

120+
[Fact]
121+
public void Should_throw_if_AllowDefaultStructs_is_disabled_on_keyed_value_object_and_value_is_null()
122+
{
122123
FluentActions.Invoking(() => RoundTrip(new GenericClass<object>(null), (GenericClass<StringBasedStructValueObject>)null))
123124
.Should().Throw<MessagePackSerializationException>()
124125
.WithInnerException<MessagePackSerializationException>().WithMessage("Cannot convert null to type \"StringBasedStructValueObject\" because it doesn't allow default values.");
@@ -359,16 +360,20 @@ public void Should_throw_if_AllowDefaultStructs_is_disabled_on_complex_value_obj
359360
}
360361

361362
[Fact]
362-
public void Should_throw_if_complex_value_object_property_has_AllowDefaultStructs_equals_to_false_and_value_is_null_or_default()
363+
public void Should_not_throw_if_complex_value_object_property_has_AllowDefaultStructs_equals_to_false_and_value_is_default()
363364
{
364365
// property is null or default
365366
FluentActions.Invoking(() => RoundTrip(new GenericClass<int>(0), ComplexValueObjectDoesNotAllowDefaultStructsWithInt.Create(0)))
366367
.Should().NotThrow();
367368

368369
FluentActions.Invoking(() => RoundTrip(new GenericClass<int>(0), ComplexValueObjectDoesNotAllowDefaultStructsWithIntBasedStruct.Create(IntBasedStructValueObjectDoesNotAllowDefaultStructs.Create(0))))
369-
.Should().Throw<MessagePackSerializationException>()
370-
.WithInnerException<MessagePackSerializationException>().WithMessage("Cannot convert the value 0 to type \"IntBasedStructValueObjectDoesNotAllowDefaultStructs\" because it doesn't allow default values.");
370+
.Should().NotThrow();
371+
}
371372

373+
[Fact]
374+
public void Should_throw_if_complex_value_object_property_has_AllowDefaultStructs_equals_to_false_and_value_is_null_or_default()
375+
{
376+
// property is null
372377
FluentActions.Invoking(() => RoundTrip(new GenericClass<object>(null), ComplexValueObjectDoesNotAllowDefaultStructsWithStringBasedStruct.Create(StringBasedStructValueObject.Create(""))))
373378
.Should().Throw<MessagePackSerializationException>()
374379
.WithInnerException<MessagePackSerializationException>().WithMessage("Cannot convert null to type \"StringBasedStructValueObject\" because it doesn't allow default values.");

test/Thinktecture.Runtime.Extensions.Newtonsoft.Json.Tests/Json/ThinktectureNewtonsoftJsonConverterTests/ReadJson.cs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ public void Should_try_deserialize_keyed_value_object_when_value_is_null_or_defa
8181

8282
// struct - int
8383
Deserialize<IntBasedStructValueObject>("0").Should().Be(IntBasedStructValueObject.Create(0)); // AllowDefaultStructs = true
84+
8485
FluentActions.Invoking(() => Deserialize<IntBasedStructValueObject>("null"))
8586
.Should().Throw<JsonException>().WithMessage("Error converting value {null} to type 'System.Int32'. Path '', line 1, position 4.");
8687

87-
FluentActions.Invoking(() => Deserialize<IntBasedStructValueObjectDoesNotAllowDefaultStructs>("0")) // AllowDefaultStructs = true
88-
.Should().Throw<JsonException>().WithMessage("Cannot convert the value 0 to type \"IntBasedStructValueObjectDoesNotAllowDefaultStructs\" because it doesn't allow default values.");
88+
Deserialize<IntBasedStructValueObjectDoesNotAllowDefaultStructs>("0").Should().Be(IntBasedStructValueObjectDoesNotAllowDefaultStructs.Create(0)); // AllowDefaultStructs = true
8989

9090
// nullable struct - string
9191
Deserialize<StringBasedStructValueObject?>("null").Should().Be(null);
@@ -99,6 +99,20 @@ public void Should_try_deserialize_keyed_value_object_when_value_is_null_or_defa
9999
.Should().Throw<JsonException>().WithMessage("Cannot convert null to type \"ReferenceTypeBasedStructValueObjectDoesNotAllowDefaultStructs\" because it doesn't allow default values.");
100100
}
101101

102+
[Fact]
103+
public void Should_not_throw_if_AllowDefaultStructs_is_disabled_on_keyed_value_object_and_value_is_default()
104+
{
105+
FluentActions.Invoking(() => Deserialize<GenericClass<IntBasedStructValueObjectDoesNotAllowDefaultStructs>>("{\"Property\": 0 }"))
106+
.Should().NotThrow();
107+
}
108+
109+
[Fact]
110+
public void Should_throw_if_AllowDefaultStructs_is_disabled_on_keyed_value_object_and_value_is_null()
111+
{
112+
FluentActions.Invoking(() => Deserialize<GenericClass<StringBasedStructValueObject>>("{\"Property\": null }"))
113+
.Should().Throw<JsonException>().WithMessage("Cannot convert null to type \"StringBasedStructValueObject\" because it doesn't allow default values.");
114+
}
115+
102116
[Fact]
103117
public void Should_deserialize_value_objects_with_NullInFactoryMethodsYieldsNull()
104118
{
@@ -263,15 +277,20 @@ public void Should_throw_if_AllowDefaultStructs_is_disabled_on_complex_value_obj
263277
}
264278

265279
[Fact]
266-
public void Should_throw_if_complex_value_object_property_has_AllowDefaultStructs_equals_to_false_and_value_is_null_or_default()
280+
public void Should_not_throw_if_complex_value_object_property_has_AllowDefaultStructs_equals_to_false_and_value_is_default()
267281
{
268282
// property is null or default
269283
FluentActions.Invoking(() => Deserialize<ComplexValueObjectDoesNotAllowDefaultStructsWithInt>("{ \"Property\": 0 }"))
270284
.Should().NotThrow();
271285

272286
FluentActions.Invoking(() => Deserialize<ComplexValueObjectDoesNotAllowDefaultStructsWithIntBasedStruct>("{ \"Property\": 0 }"))
273-
.Should().Throw<JsonException>().WithMessage("Cannot convert the value 0 to type \"IntBasedStructValueObjectDoesNotAllowDefaultStructs\" because it doesn't allow default values.");
287+
.Should().NotThrow();
288+
}
274289

290+
[Fact]
291+
public void Should_throw_if_complex_value_object_property_has_AllowDefaultStructs_equals_to_false_and_value_is_null_or_missing()
292+
{
293+
// property is null
275294
FluentActions.Invoking(() => Deserialize<ComplexValueObjectDoesNotAllowDefaultStructsWithStringBasedStruct>("{ \"Property\": null }"))
276295
.Should().Throw<JsonException>().WithMessage("Cannot convert null to type \"StringBasedStructValueObject\" because it doesn't allow default values.");
277296

0 commit comments

Comments
 (0)