Skip to content

Commit f726ed4

Browse files
committed
Added empty-string handling for keyed value objects (EmptyStringInFactoryMethodsYieldsNull)
1 parent 2c5fcc2 commit f726ed4

File tree

19 files changed

+1066
-81
lines changed

19 files changed

+1066
-81
lines changed

samples/Thinktecture.Runtime.Extensions.AspNetCore.Samples/Controllers/DemoController.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,45 @@ public IActionResult RoundTrip(ProductTypeWithJsonConverter productType)
6969
}
7070

7171
[HttpGet("productName/{name}")]
72-
public IActionResult RoundTrip(ProductName name)
72+
public IActionResult RoundTrip(ProductName? name)
7373
{
7474
if (!ModelState.IsValid)
7575
return BadRequest(ModelState);
7676

77-
_logger.LogInformation("Round trip test with {Type}: {Name}", name.GetType().Name, name);
77+
_logger.LogInformation("Round trip test with {Type}: {Name}", name?.GetType().Name, name);
7878

7979
return Json(name);
8080
}
8181

8282
[HttpPost("productName")]
83-
public IActionResult RoundTripPost([FromBody] ProductName name)
83+
public IActionResult RoundTripPost([FromBody] ProductName? name)
8484
{
8585
if (!ModelState.IsValid)
8686
return BadRequest(ModelState);
8787

88-
_logger.LogInformation("Round trip test with {Type}: {Name}", name.GetType().Name, name);
88+
_logger.LogInformation("Round trip test with {Type}: {Name}", name?.GetType().Name, name);
89+
90+
return Json(name);
91+
}
92+
93+
[HttpGet("otherProductName/{name}")]
94+
public IActionResult RoundTrip(OtherProductName? name)
95+
{
96+
if (!ModelState.IsValid)
97+
return BadRequest(ModelState);
98+
99+
_logger.LogInformation("Round trip test with {Type}: {Name}", name?.GetType().Name, name);
100+
101+
return Json(name);
102+
}
103+
104+
[HttpPost("otherProductName")]
105+
public IActionResult RoundTripPost([FromBody] OtherProductName? name)
106+
{
107+
if (!ModelState.IsValid)
108+
return BadRequest(ModelState);
109+
110+
_logger.LogInformation("Round trip test with {Type}: {Name}", name?.GetType().Name, name);
89111

90112
return Json(name);
91113
}

samples/Thinktecture.Runtime.Extensions.AspNetCore.Samples/Program.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Net.Http;
23
using System.Net.Http.Json;
34
using System.Text;
@@ -58,10 +59,20 @@ private static async Task DoHttpRequestsAsync(ILogger logger)
5859
await DoRequestAsync(logger, client, "productTypeWrapper", new { ProductType = "invalid" }); // invalid
5960
await DoRequestAsync(logger, client, "productTypeWithJsonConverter/groceries");
6061
await DoRequestAsync(logger, client, "productTypeWithJsonConverter/invalid"); // invalid
62+
6163
await DoRequestAsync(logger, client, "productName/bread");
62-
await DoRequestAsync(logger, client, "productName/a"); // invalid
64+
await DoRequestAsync(logger, client, "productName/a"); // invalid (Product name cannot be 1 character long.)
65+
await DoRequestAsync(logger, client, "productName/%20"); // product name is null
6366
await DoRequestAsync(logger, client, "productName", "bread");
64-
await DoRequestAsync(logger, client, "productName", "a"); // invalid
67+
await DoRequestAsync(logger, client, "productName", "a"); // invalid (Product name cannot be 1 character long.)
68+
await DoRequestAsync(logger, client, "productName", " "); // invalid (Product name cannot be empty.)
69+
70+
await DoRequestAsync(logger, client, "otherProductName/bread");
71+
await DoRequestAsync(logger, client, "otherProductName/a"); // invalid (Product name cannot be 1 character long.)
72+
await DoRequestAsync(logger, client, "otherProductName/%20"); // product name is null
73+
await DoRequestAsync(logger, client, "otherProductName", "bread");
74+
await DoRequestAsync(logger, client, "otherProductName", "a"); // invalid (Product name cannot be 1 character long.)
75+
6576
await DoRequestAsync(logger, client, "boundary", BoundaryWithJsonConverter.Create(1, 2));
6677
await DoRequestAsync(logger, client, "boundary", jsonBody: "{ \"lower\": 2, \"upper\": 1 }");
6778
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.ComponentModel.DataAnnotations;
3+
4+
namespace Thinktecture.ValueObjects;
5+
6+
[ValueObject(EmptyStringInFactoryMethodsYieldsNull = true)]
7+
public sealed partial class OtherProductName
8+
{
9+
[ValueObjectEqualityMember(EqualityComparer = nameof(StringComparer.OrdinalIgnoreCase))]
10+
private string Value { get; }
11+
12+
static partial void ValidateFactoryArguments(ref ValidationResult? validationResult, ref string value)
13+
{
14+
if (String.IsNullOrWhiteSpace(value))
15+
{
16+
validationResult = new ValidationResult("Product name cannot be empty.");
17+
return;
18+
}
19+
20+
if (value.Length == 1)
21+
{
22+
validationResult = new ValidationResult("Product name cannot be 1 character long.");
23+
return;
24+
}
25+
26+
value = value.Trim();
27+
}
28+
}
Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,48 @@
1-
using System.ComponentModel.DataAnnotations;
2-
using Serilog;
3-
4-
namespace Thinktecture.ValueObjects;
5-
6-
public class ValueObjectDemos
7-
{
8-
public static void Demo(ILogger logger)
9-
{
10-
logger.Information("==== Demo for ValueObjectAttribute ====");
11-
12-
var bread = ProductName.Create("Bread");
13-
logger.Information("Product name: {Bread}", bread);
14-
15-
string valueOfTheProductName = bread;
16-
logger.Information("Implicit conversion of ProductName -> string: {Key}", valueOfTheProductName);
17-
18-
try
19-
{
20-
ProductName.Create(" ");
21-
logger.Warning("This line won't be reached.");
22-
}
23-
catch (ValidationException)
24-
{
25-
logger.Information("ValidationException is thrown because a product name cannot be an empty string.");
26-
}
27-
28-
var validationResult = ProductName.TryCreate("Milk", out var milk);
29-
if (validationResult == ValidationResult.Success)
30-
logger.Information("Product name '{Name}' created with 'TryCreate'.", milk);
31-
32-
// Thanks to setting "NullInFactoryMethodsYieldsNull = true"
33-
var nullProduct = ProductName.Create(null);
34-
logger.Information("Null-Product name: {NullProduct}", nullProduct);
35-
36-
var nullValidationResult = ProductName.TryCreate(null, out nullProduct);
37-
if (nullValidationResult == ValidationResult.Success)
38-
logger.Information("Null-Product name: {NullProduct}", nullProduct);
39-
}
40-
}
1+
using System.ComponentModel.DataAnnotations;
2+
using Serilog;
3+
4+
namespace Thinktecture.ValueObjects;
5+
6+
public class ValueObjectDemos
7+
{
8+
public static void Demo(ILogger logger)
9+
{
10+
logger.Information("==== Demo for ValueObjectAttribute ====");
11+
12+
var bread = ProductName.Create("Bread");
13+
logger.Information("Product name: {Bread}", bread);
14+
15+
string valueOfTheProductName = bread;
16+
logger.Information("Implicit conversion of ProductName -> string: {Key}", valueOfTheProductName);
17+
18+
try
19+
{
20+
ProductName.Create(" ");
21+
logger.Warning("This line won't be reached.");
22+
}
23+
catch (ValidationException)
24+
{
25+
logger.Information("ValidationException is thrown because a product name cannot be an empty string.");
26+
}
27+
28+
var validationResult = ProductName.TryCreate("Milk", out var milk);
29+
if (validationResult == ValidationResult.Success)
30+
logger.Information("Product name '{Name}' created with 'TryCreate'.", milk);
31+
32+
// Thanks to setting "NullInFactoryMethodsYieldsNull = true" the method "Create" returns null
33+
var nullProduct = ProductName.Create(null);
34+
logger.Information("Null-Product name: {NullProduct}", nullProduct);
35+
36+
// Thanks to setting "EmptyStringInFactoryMethodsYieldsNull = true" the method "Create" returns null
37+
var otherNullProductName = OtherProductName.Create(null);
38+
logger.Information("Null-Product name: {NullProduct}", otherNullProductName);
39+
40+
// Thanks to setting "EmptyStringInFactoryMethodsYieldsNull = true" the method "Create" returns null
41+
var otherNullProductName2 = OtherProductName.Create(" ");
42+
logger.Information("Null-Product name: {NullProduct}", otherNullProductName2);
43+
44+
var nullValidationResult = ProductName.TryCreate(null, out nullProduct);
45+
if (nullValidationResult == ValidationResult.Success)
46+
logger.Information("Null-Product name: {NullProduct}", nullProduct);
47+
}
48+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Thinktecture.Text.Json.Serialization;
1212
public sealed class ValueObjectJsonConverter<T, TKey> : JsonConverter<T>
1313
where TKey : notnull
1414
{
15-
private readonly Func<TKey, T> _convertFromKey;
15+
private readonly Func<TKey, T?> _convertFromKey;
1616
private readonly Func<T, TKey> _convertToKey;
1717
private readonly JsonConverter<TKey> _keyConverter;
1818

@@ -23,7 +23,7 @@ public sealed class ValueObjectJsonConverter<T, TKey> : JsonConverter<T>
2323
/// <param name="convertToKey">Converts an instance of type <typeparamref name="T"/> to an instance of <typeparamref name="TKey"/>.</param>
2424
/// <param name="options">JSON serializer options.</param>
2525
public ValueObjectJsonConverter(
26-
Func<TKey, T> convertFromKey,
26+
Func<TKey, T?> convertFromKey,
2727
Func<T, TKey> convertToKey,
2828
JsonSerializerOptions options)
2929
{

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Thinktecture.Formatters;
1111
public abstract class ValueObjectMessagePackFormatterBase<T, TKey> : IMessagePackFormatter<T>
1212
where TKey : notnull
1313
{
14-
private readonly Func<TKey, T> _convertFromKey;
14+
private readonly Func<TKey, T?> _convertFromKey;
1515
private readonly Func<T, TKey> _convertToKey;
1616

1717
/// <summary>
@@ -20,7 +20,7 @@ public abstract class ValueObjectMessagePackFormatterBase<T, TKey> : IMessagePac
2020
/// <param name="convertFromKey">Converts an instance of type <typeparamref name="TKey"/> to an instance of <typeparamref name="T"/>.</param>
2121
/// <param name="convertToKey">Converts an instance of type <typeparamref name="T"/> to an instance of <typeparamref name="TKey"/>.</param>
2222
protected ValueObjectMessagePackFormatterBase(
23-
Func<TKey, T> convertFromKey,
23+
Func<TKey, T?> convertFromKey,
2424
Func<T, TKey> convertToKey)
2525
{
2626
_convertFromKey = convertFromKey ?? throw new ArgumentNullException(nameof(convertFromKey));

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public abstract class ValueObjectNewtonsoftJsonConverterBase<T, TKey> : JsonConv
1515
private static readonly Type _type = typeof(T);
1616
private static readonly Type _keyType = typeof(TKey);
1717

18-
private readonly Func<TKey, T> _convertFromKey;
18+
private readonly Func<TKey, T?> _convertFromKey;
1919
private readonly Func<T, TKey> _convertToKey;
2020

2121
/// <summary>
@@ -24,7 +24,7 @@ public abstract class ValueObjectNewtonsoftJsonConverterBase<T, TKey> : JsonConv
2424
/// <param name="convertFromKey">Converts an instance of type <typeparamref name="TKey"/> to an instance of <typeparamref name="T"/>.</param>
2525
/// <param name="convertToKey">Converts an instance of type <typeparamref name="T"/> to an instance of <typeparamref name="TKey"/>.</param>
2626
protected ValueObjectNewtonsoftJsonConverterBase(
27-
Func<TKey, T> convertFromKey,
27+
Func<TKey, T?> convertFromKey,
2828
Func<T, TKey> convertToKey)
2929
{
3030
_convertFromKey = convertFromKey ?? throw new ArgumentNullException(nameof(convertFromKey));

0 commit comments

Comments
 (0)