Skip to content

Commit 2dde911

Browse files
committed
Added an example how ad hoc union can be made serializable and asp.net core model bindable
1 parent 852fc46 commit 2dde911

7 files changed

Lines changed: 207 additions & 2 deletions

File tree

docs

Submodule docs updated from 365caf2 to a480f5d

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.AspNetCore.Mvc;
55
using Microsoft.Extensions.Logging;
66
using Thinktecture.SmartEnums;
7+
using Thinktecture.Unions;
78
using Thinktecture.ValueObjects;
89

910
namespace Thinktecture.Controllers;
@@ -131,6 +132,17 @@ public IActionResult RoundTripPost([FromBody] OpenEndDate endDate)
131132
return Json(endDate);
132133
}
133134

135+
[HttpGet("textOrNumber/{textOrNumber}")]
136+
public IActionResult RoundTrip(TextOrNumberSerializable textOrNumber)
137+
{
138+
if (!ModelState.IsValid)
139+
return BadRequest(ModelState);
140+
141+
_logger.LogInformation("Round trip test with {Type}: {EndDate}", textOrNumber.GetType().Name, textOrNumber);
142+
143+
return Json(textOrNumber);
144+
}
145+
134146
private IActionResult RoundTripValidatableEnum<T>(T value)
135147
where T : IValidatableEnum
136148
{

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Thinktecture.Helpers;
1818
using Thinktecture.SmartEnums;
1919
using Thinktecture.Text.Json.Serialization;
20+
using Thinktecture.Unions;
2021
using Thinktecture.Validation;
2122
using Thinktecture.ValueObjects;
2223
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -82,6 +83,8 @@ private static async Task DoHttpRequestsAsync(ILogger logger, bool forMinimalWeb
8283
await DoRequestAsync(logger, client, $"enddate/{DateOnly.FromDateTime(DateTime.Now):O}");
8384
await DoRequestAsync(logger, client, "enddate", DateOnly.FromDateTime(DateTime.Now));
8485

86+
await DoRequestAsync(logger, client, "textOrNumber/Number|42");
87+
8588
await DoRequestAsync(logger, client, "notification/channels");
8689
await DoRequestAsync(logger, client, "notification/channels/email", "Test email");
8790
}
@@ -179,6 +182,8 @@ private static Task StartMinimalWebApiAsync(ILoggerFactory loggerFactory)
179182
routeGroup.MapGet("enddate/{date}", (OpenEndDate date) => date);
180183
routeGroup.MapPost("enddate", ([FromBody] OpenEndDate date) => date);
181184

185+
routeGroup.MapGet("textOrNumber/{textOrNumber}", (TextOrNumberSerializable textOrNumber) => textOrNumber);
186+
182187
routeGroup.MapGet("notification/channels", () =>
183188
{
184189
var channels = NotificationChannelTypeDto.Items.Select(c => c.Name);

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using MessagePack.Resolvers;
55
using Thinktecture;
66
using Thinktecture.SmartEnums;
7+
using Thinktecture.Unions;
78
using Thinktecture.ValueObjects;
89

910
var groceries = DoRoundTripSerializationOfTypesWithoutFormatter(ProductType.Groceries);
@@ -23,6 +24,13 @@
2324
var boundaryWithFormatter = DoRoundTripSerialization(BoundaryWithMessagePackFormatter.Create(1, 2));
2425
Console.WriteLine(boundaryWithFormatter);
2526

27+
// Ad hoc union
28+
var textOrNumber = DoRoundTripSerializationOfTypesWithoutFormatter((TextOrNumberSerializable)42);
29+
Console.WriteLine(textOrNumber);
30+
31+
var textOrNumberWithFormatter = DoRoundTripSerialization((TextOrNumberSerializableWithFormatter)42);
32+
Console.WriteLine(textOrNumberWithFormatter);
33+
2634
// Smart Enums and Value Objects without [MessagePackFormatterAttribute] need "ValueObjectMessageFormatterResolver".
2735
static T DoRoundTripSerializationOfTypesWithoutFormatter<T>(T obj)
2836
{
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
using MessagePack;
4+
5+
namespace Thinktecture;
6+
7+
[Union<string, int>(T1IsNullableReferenceType = true,
8+
T1Name = "Text",
9+
T2Name = "Number",
10+
SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads,
11+
MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)]
12+
[ValueObjectFactory<string>(UseForSerialization = SerializationFrameworks.All)]
13+
[MessagePackFormatter(typeof(Formatters.ValueObjectMessagePackFormatter<TextOrNumberSerializableWithFormatter, string, ValidationError>))] // Optional: otherwise ValueObjectMessageFormatterResolver is required
14+
public partial class TextOrNumberSerializableWithFormatter :
15+
IValueObjectFactory<TextOrNumberSerializableWithFormatter, string, ValidationError>, // For deserialization
16+
IValueObjectConvertable<string>, // For serialization
17+
IParsable<TextOrNumberSerializableWithFormatter> // For Minimal API and ASP.NET Core model binding validation
18+
{
19+
// For serialization
20+
public string ToValue()
21+
{
22+
return Switch(text: t => $"Text|{t}",
23+
number: n => $"Number|{n}");
24+
}
25+
26+
// For deserialization
27+
public static ValidationError? Validate(string? value, IFormatProvider? provider, out TextOrNumberSerializableWithFormatter? item)
28+
{
29+
if (String.IsNullOrWhiteSpace(value))
30+
{
31+
item = null;
32+
return null;
33+
}
34+
35+
if (value.StartsWith("Text|", StringComparison.OrdinalIgnoreCase))
36+
{
37+
item = value.Substring(5);
38+
return null;
39+
}
40+
41+
if (value.StartsWith("Number|", StringComparison.OrdinalIgnoreCase))
42+
{
43+
if (Int32.TryParse(value.Substring(7), out var number))
44+
{
45+
item = number;
46+
return null;
47+
}
48+
49+
item = null;
50+
return new ValidationError("Invalid number format");
51+
}
52+
53+
item = null;
54+
return new ValidationError("Invalid format");
55+
}
56+
57+
public static TextOrNumberSerializableWithFormatter Parse(string s, IFormatProvider? provider)
58+
{
59+
var validationError = Validate(s, provider, out var result);
60+
61+
if (validationError is null)
62+
return result!;
63+
64+
throw new FormatException(validationError.Message);
65+
}
66+
67+
public static bool TryParse(
68+
[NotNullWhen(true)] string? s,
69+
IFormatProvider? provider,
70+
[MaybeNullWhen(false)] out TextOrNumberSerializableWithFormatter result)
71+
{
72+
if (s is null)
73+
{
74+
result = null;
75+
return false;
76+
}
77+
78+
var validationError = Validate(s, provider, out result!);
79+
return validationError is null;
80+
}
81+
}

samples/Thinktecture.Runtime.Extensions.Samples/Unions/DiscriminatedUnionsDemos.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.ComponentModel.DataAnnotations;
33
using System.Text.Json;
44
using Serilog;
5+
using Thinktecture.Text.Json.Serialization;
56

67
namespace Thinktecture.Unions;
78

@@ -10,6 +11,7 @@ public class DiscriminatedUnionsDemos
1011
public static void Demo(ILogger logger)
1112
{
1213
DemoForAdHocUnions(logger);
14+
SerializationDemoForAdHocUnions(logger);
1315
DemoForUnions(logger);
1416
DemoForJurisdiction(logger);
1517
DemoForPartiallyKnownDate(logger);
@@ -82,6 +84,22 @@ private static void DemoForAdHocUnions(ILogger logger)
8284
logger.Information("{Response}", mapPartiallyResponse);
8385
}
8486

87+
private static void SerializationDemoForAdHocUnions(ILogger logger)
88+
{
89+
TextOrNumberSerializable textOrNumberFromString = "text";
90+
TextOrNumberSerializable textOrNumberFromInt = 42;
91+
92+
var jsonOptions = new JsonSerializerOptions { Converters = { new ValueObjectJsonConverterFactory() } };
93+
94+
var json = JsonSerializer.Serialize(textOrNumberFromString, jsonOptions);
95+
var deserializedTextOrNumber = JsonSerializer.Deserialize<TextOrNumberSerializable>(json, jsonOptions);
96+
logger.Information("TextOrNumberSerializable (de)serialization: {Json} -> {TextOrNumber}", json, deserializedTextOrNumber);
97+
98+
json = JsonSerializer.Serialize(textOrNumberFromInt, jsonOptions);
99+
deserializedTextOrNumber = JsonSerializer.Deserialize<TextOrNumberSerializable>(json, jsonOptions);
100+
logger.Information("TextOrNumberSerializable (de)serialization: {Json} -> {TextOrNumber}", json, deserializedTextOrNumber);
101+
}
102+
85103
private static void DemoForUnions(ILogger logger)
86104
{
87105
DemoForUnionsUsingClass(logger);
@@ -253,7 +271,7 @@ private static void DemoForPartiallyKnownDate(ILogger logger)
253271
var preciseDate = new PartiallyKnownDate.Date(2023, 12, 31);
254272

255273
// Implicit conversion from DateOnly to PartiallyKnownDate
256-
PartiallyKnownDate fullDate = new DateOnly(2024, 3, 15); // Date(2024, 3, 15)
274+
PartiallyKnownDate fullDate = new DateOnly(2024, 3, 15); // Date(2024, 3, 15)
257275

258276
static string FormatDate(PartiallyKnownDate? date)
259277
{
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Text.Json.Serialization;
4+
using Thinktecture.Text.Json.Serialization;
5+
6+
namespace Thinktecture.Unions;
7+
8+
[Union<string, int>(T1Name = "Text",
9+
T2Name = "Number",
10+
SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads,
11+
MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)]
12+
[ValueObjectFactory<string>(UseForSerialization = SerializationFrameworks.All)]
13+
[JsonConverter(typeof(ValueObjectJsonConverterFactory<TextOrNumberSerializable, string, ValidationError>))] // Optional: otherwise ValueObjectJsonConverterFactory is required
14+
public partial class TextOrNumberSerializable :
15+
IValueObjectFactory<TextOrNumberSerializable, string, ValidationError>, // For deserialization
16+
IValueObjectConvertable<string>, // For serialization
17+
IParsable<TextOrNumberSerializable> // For Minimal API and ASP.NET Core model binding validation
18+
{
19+
// For serialization
20+
public string ToValue()
21+
{
22+
return Switch(text: t => $"Text|{t}",
23+
number: n => $"Number|{n}");
24+
}
25+
26+
// For deserialization
27+
public static ValidationError? Validate(string? value, IFormatProvider? provider, out TextOrNumberSerializable? item)
28+
{
29+
if (String.IsNullOrWhiteSpace(value))
30+
{
31+
item = null;
32+
return null;
33+
}
34+
35+
if (value.StartsWith("Text|", StringComparison.OrdinalIgnoreCase))
36+
{
37+
item = value.Substring(5);
38+
return null;
39+
}
40+
41+
if (value.StartsWith("Number|", StringComparison.OrdinalIgnoreCase))
42+
{
43+
if (Int32.TryParse(value.Substring(7), out var number))
44+
{
45+
item = number;
46+
return null;
47+
}
48+
49+
item = null;
50+
return new ValidationError("Invalid number format");
51+
}
52+
53+
item = null;
54+
return new ValidationError("Invalid format");
55+
}
56+
57+
public static TextOrNumberSerializable Parse(string s, IFormatProvider? provider)
58+
{
59+
var validationError = Validate(s, provider, out var result);
60+
61+
if (validationError is null)
62+
return result!;
63+
64+
throw new FormatException(validationError.Message);
65+
}
66+
67+
public static bool TryParse(
68+
[NotNullWhen(true)] string? s,
69+
IFormatProvider? provider,
70+
[MaybeNullWhen(false)] out TextOrNumberSerializable result)
71+
{
72+
if (s is null)
73+
{
74+
result = null;
75+
return false;
76+
}
77+
78+
var validationError = Validate(s, provider, out result!);
79+
return validationError is null;
80+
}
81+
}

0 commit comments

Comments
 (0)