Skip to content

Commit cd16d7e

Browse files
committed
Value passed to ad hoc union ctor/factory can be normalized via "partial method"
1 parent 93f9360 commit cd16d7e

107 files changed

Lines changed: 2209 additions & 3 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs

Submodule docs updated from e745709 to 75405d3

src/Thinktecture.Runtime.Extensions.Roslyn.Sources/CodeAnalysis/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public static class Methods
4444
public const string GET = "Get";
4545
public const string MAP = "Map";
4646
public const string MAP_PARTIALLY = "MapPartially";
47+
public const string NORMALIZE = "Normalize";
4748
public const string SWITCH = "Switch";
4849
public const string SWITCH_PARTIALLY = "SwitchPartially";
4950
public const string TO_STRING = "ToString";

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionCodeGenerator.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,31 @@ private void GenerateUnion(CancellationToken cancellationToken)
151151
if (!_state.Settings.SkipToString)
152152
GenerateToString();
153153

154+
GenerateNormalizePartialDeclarations();
155+
154156
_sb.Append(@"
155157
}");
156158
}
157159

160+
private void GenerateNormalizePartialDeclarations()
161+
{
162+
foreach (var memberType in _state.MemberTypes)
163+
{
164+
if (memberType.Setting.IsStateless)
165+
continue;
166+
167+
// No call site: with FactoryMethodGeneration = None and TypeDuplicateCounter != 0,
168+
// no factory is generated and the indexed ctor is private. Skip the declaration.
169+
if (!_needsFactoryMethods && memberType.TypeDuplicateCounter != 0)
170+
continue;
171+
172+
_sb.Append(@"
173+
174+
").Append(GENERATED_CODE_ATTRIBUTE).Append(@"
175+
static partial void ").Append(Constants.Methods.NORMALIZE).Append(memberType.Name).Append("(ref ").AppendTypeFullyQualified(memberType).Append(" ").AppendEscaped(memberType.ArgumentName).Append(");");
176+
}
177+
}
178+
158179
private void GenerateFactoryMethods()
159180
{
160181
if (!_needsFactoryMethods)
@@ -220,7 +241,15 @@ private void GenerateFactoryMethod(AdHocUnionMemberTypeState memberType, int mem
220241
}
221242

222243
_sb.Append(@")
244+
{");
245+
246+
if (memberType.TypeDuplicateCounter != 0 && !memberType.Setting.IsStateless)
223247
{
248+
_sb.Append(@"
249+
").Append(Constants.Methods.NORMALIZE).Append(memberType.Name).Append("(ref ").AppendEscaped(memberType.ArgumentName).Append(");");
250+
}
251+
252+
_sb.Append(@"
224253
return new ").AppendTypeFullyQualified(_state).Append("(");
225254

226255
if (memberType.Setting.IsStateless)
@@ -1047,6 +1076,12 @@ private void GenerateConstructors()
10471076
_sb.Append(@")
10481077
{");
10491078

1079+
if (!needsIndexedConstructor && !memberType.Setting.IsStateless)
1080+
{
1081+
_sb.Append(@"
1082+
").Append(Constants.Methods.NORMALIZE).Append(memberType.Name).Append("(ref ").AppendEscaped(memberType.ArgumentName).Append(");");
1083+
}
1084+
10501085
if (!memberType.Setting.IsStateless)
10511086
{
10521087
_sb.Append(@"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#nullable enable
2+
using System;
3+
using Thinktecture.EntityFrameworkCore.Storage.ValueConversion;
4+
5+
namespace Thinktecture.Runtime.Tests.EntityFrameworkCore;
6+
7+
// ReSharper disable InconsistentNaming
8+
public class NormalizeMemberRoundTripTests
9+
{
10+
[Fact]
11+
public void Should_normalize_value_when_reading_back_via_EF_value_converter()
12+
{
13+
// The ThinktectureValueConverter routes deserialization through T.Validate(),
14+
// which constructs the union via ctor / factory. Normalize fires on that path.
15+
// The plan asserts: "EF Core's useConstructorForRead=false skip-path does not apply
16+
// to ad-hoc unions (no ConvertFromKeyExpressionViaConstructor is generated for them),
17+
// so Normalize always fires on EF read."
18+
var converter = ThinktectureValueConverterFactory.Create<NormalizingEfUnion, string>();
19+
20+
var convertFromProvider = converter.ConvertFromProvider;
21+
22+
var converted = (NormalizingEfUnion?)convertFromProvider(" Hello ");
23+
24+
converted.Should().NotBeNull();
25+
converted!.AsString.Should().Be("hello");
26+
}
27+
28+
[Fact]
29+
public void Should_normalize_value_when_reading_back_with_useConstructorForRead_false()
30+
{
31+
// Explicitly verifies that the useConstructorForRead=false path also normalizes
32+
// for ad-hoc unions (no ConvertFromKeyExpressionViaConstructor is generated for them).
33+
var converter = ThinktectureValueConverterFactory.Create<NormalizingEfUnion, string>(useConstructorForRead: false);
34+
35+
var converted = (NormalizingEfUnion?)converter.ConvertFromProvider(" Hello ");
36+
37+
converted.Should().NotBeNull();
38+
converted!.AsString.Should().Be("hello");
39+
}
40+
}
41+
42+
[Union<string, int>]
43+
[ObjectFactory<string>(UseForSerialization = SerializationFrameworks.All)]
44+
public partial class NormalizingEfUnion
45+
{
46+
public string ToValue()
47+
{
48+
return Switch(@string: t => t,
49+
@int32: n => n.ToString(System.Globalization.CultureInfo.InvariantCulture));
50+
}
51+
52+
public static ValidationError? Validate(string? value, IFormatProvider? provider, out NormalizingEfUnion? item)
53+
{
54+
if (String.IsNullOrWhiteSpace(value))
55+
{
56+
item = null;
57+
return null;
58+
}
59+
60+
item = value!;
61+
return null;
62+
}
63+
64+
static partial void NormalizeString(ref string @string)
65+
{
66+
@string = @string.Trim().ToLowerInvariant();
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#nullable enable
2+
using System;
3+
using System.Text.Json;
4+
using Thinktecture.Text.Json.Serialization;
5+
6+
namespace Thinktecture.Runtime.Tests.Text.Json.Serialization.ThinktectureJsonConverterFactoryTests;
7+
8+
// ReSharper disable InconsistentNaming
9+
public class NormalizeMemberRoundTrip
10+
{
11+
[Fact]
12+
public void Should_apply_NormalizeMember_during_JSON_deserialization_round_trip()
13+
{
14+
var options = new JsonSerializerOptions { Converters = { new ThinktectureJsonConverterFactory() } };
15+
16+
var json = "\" Hello \"";
17+
18+
var deserialized = JsonSerializer.Deserialize<NormalizingSerializableUnion>(json, options);
19+
20+
deserialized.Should().NotBeNull();
21+
deserialized!.AsString.Should().Be("hello");
22+
}
23+
24+
[Fact]
25+
public void Should_apply_NormalizeMember_during_JSON_serialize_then_deserialize()
26+
{
27+
var options = new JsonSerializerOptions { Converters = { new ThinktectureJsonConverterFactory() } };
28+
29+
var original = new NormalizingSerializableUnion(" WORLD "); // ctor normalizes to "world"
30+
var json = JsonSerializer.Serialize(original, options);
31+
32+
json.Should().Be("\"world\"");
33+
34+
var roundTripped = JsonSerializer.Deserialize<NormalizingSerializableUnion>(json, options);
35+
roundTripped.Should().Be(original);
36+
}
37+
}
38+
39+
[Union<string, int>]
40+
[ObjectFactory<string>(UseForSerialization = SerializationFrameworks.All)]
41+
public partial class NormalizingSerializableUnion
42+
{
43+
public string ToValue()
44+
{
45+
return Switch(@string: t => t,
46+
@int32: n => n.ToString(System.Globalization.CultureInfo.InvariantCulture));
47+
}
48+
49+
public static ValidationError? Validate(string? value, IFormatProvider? provider, out NormalizingSerializableUnion? item)
50+
{
51+
if (String.IsNullOrWhiteSpace(value))
52+
{
53+
item = null;
54+
return null;
55+
}
56+
57+
item = value!;
58+
return null;
59+
}
60+
61+
static partial void NormalizeString(ref string @string)
62+
{
63+
@string = @string.Trim().ToLowerInvariant();
64+
}
65+
}

test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_apply_cascade_when_SingleBackingFieldType_set_without_UseSingleBackingField_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ namespace Thinktecture.Tests
8080
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Thinktecture.Runtime.Extensions.SourceGenerator", "10.3.0.0")]
8181
public TestUnion(global::Thinktecture.Tests.Foo1 @foo1)
8282
{
83+
NormalizeFoo1(ref @foo1);
8384
this._obj = @foo1;
8485
this._valueIndex = 1;
8586
}
@@ -91,6 +92,7 @@ namespace Thinktecture.Tests
9192
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Thinktecture.Runtime.Extensions.SourceGenerator", "10.3.0.0")]
9293
public TestUnion(global::Thinktecture.Tests.Foo2 @foo2)
9394
{
95+
NormalizeFoo2(ref @foo2);
9496
this._obj = @foo2;
9597
this._valueIndex = 2;
9698
}
@@ -368,5 +370,11 @@ namespace Thinktecture.Tests
368370
_ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.")
369371
};
370372
}
373+
374+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Thinktecture.Runtime.Extensions.SourceGenerator", "10.3.0.0")]
375+
static partial void NormalizeFoo1(ref global::Thinktecture.Tests.Foo1 @foo1);
376+
377+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Thinktecture.Runtime.Extensions.SourceGenerator", "10.3.0.0")]
378+
static partial void NormalizeFoo2(ref global::Thinktecture.Tests.Foo2 @foo2);
371379
}
372380
}

0 commit comments

Comments
 (0)