Skip to content

Commit e438edf

Browse files
committed
improved examples
1 parent 2ebc172 commit e438edf

File tree

12 files changed

+348
-50
lines changed

12 files changed

+348
-50
lines changed

docs

Submodule docs updated from b65b3aa to 0d6f549

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public IActionResult RoundTrip([FromBody] BoundaryWithJsonConverter boundary)
137137
}
138138

139139
[HttpGet("enddate/{endDate}")]
140-
public IActionResult RoundTripGet(EndDate endDate)
140+
public IActionResult RoundTripGet(OpenEndDate endDate)
141141
{
142142
if (!ModelState.IsValid)
143143
return BadRequest(ModelState);
@@ -148,7 +148,7 @@ public IActionResult RoundTripGet(EndDate endDate)
148148
}
149149

150150
[HttpPost("enddate")]
151-
public IActionResult RoundTripPost([FromBody] EndDate endDate)
151+
public IActionResult RoundTripPost([FromBody] OpenEndDate endDate)
152152
{
153153
if (!ModelState.IsValid)
154154
return BadRequest(ModelState);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ private static Task StartMinimalWebApiAsync(ILoggerFactory loggerFactory)
175175
routeGroup.MapPost("otherProductName", ([FromBody] OtherProductName name) => name);
176176
routeGroup.MapPost("boundary", ([FromBody] BoundaryWithJsonConverter boundary) => boundary);
177177

178-
routeGroup.MapGet("enddate/{date}", (EndDate date) => date);
179-
routeGroup.MapPost("enddate", ([FromBody] EndDate date) => date);
178+
routeGroup.MapGet("enddate/{date}", (OpenEndDate date) => date);
179+
routeGroup.MapPost("enddate", ([FromBody] OpenEndDate date) => date);
180180

181181
return app.StartAsync();
182182
}

samples/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Samples/Product.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ public class Product
1010
public ProductName Name { get; private set; }
1111
public ProductCategory Category { get; private set; }
1212
public ProductType ProductType { get; private set; }
13-
public EndDate EndDate { get; set; }
13+
public OpenEndDate EndDate { get; set; }
1414

1515
private Boundary? _boundary;
1616
public Boundary Boundary => _boundary ?? throw new InvalidOperationException("Boundary is not loaded.");
1717

18-
private Product(Guid id, ProductName name, ProductCategory category, ProductType productType, EndDate endDate)
18+
private Product(Guid id, ProductName name, ProductCategory category, ProductType productType, OpenEndDate endDate)
1919
{
2020
Id = id;
2121
Name = name;
@@ -30,7 +30,7 @@ public Product(
3030
ProductCategory category,
3131
ProductType productType,
3232
Boundary boundary,
33-
EndDate endDate = default)
33+
OpenEndDate endDate = default)
3434
: this(id, name, category, productType, endDate)
3535
{
3636
_boundary = boundary;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ private static async Task DoDatabaseRequestsAsync(IServiceProvider serviceProvid
4343
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
4444
var ctx = scope.ServiceProvider.GetRequiredService<ProductsDbContext>();
4545

46-
var today = (EndDate)DateOnly.FromDateTime(DateTime.Today);
46+
var today = OpenEndDate.Create(DateTime.Today);
4747

4848
await InsertProductAsync(ctx, new Product(Guid.NewGuid(), ProductName.Create("Apple"), ProductCategory.Fruits, ProductType.Groceries, Boundary.Create(1, 2), today));
4949

@@ -75,7 +75,7 @@ private static async Task DoDatabaseRequestsAsync(IServiceProvider serviceProvid
7575
logger.LogInformation("Products with End Data > Today: {@Products}", products);
7676

7777
var product = ctx.Products.Single();
78-
product.EndDate = EndDate.Infinite;
78+
product.EndDate = OpenEndDate.Infinite;
7979
await ctx.SaveChangesAsync();
8080

8181
// same query as the previous one but now "EndDate" equals "infinite" and (infinite > today) evaluates to true

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using System;
2+
using System.ComponentModel.DataAnnotations;
3+
using System.Text.Json;
14
using Serilog;
25

36
namespace Thinktecture.Unions;
@@ -8,6 +11,7 @@ public static void Demo(ILogger logger)
811
{
912
DemoForAdHocUnions(logger);
1013
DemoForUnions(logger);
14+
DemoForJurisdiction(logger);
1115
}
1216

1317
private static void DemoForAdHocUnions(ILogger logger)
@@ -177,4 +181,47 @@ void Print<T>(Result<T> r)
177181
success: s => logger.Information("[Switch] Success: {Success}", s));
178182
}
179183
}
184+
185+
private static void DemoForJurisdiction(ILogger logger)
186+
{
187+
logger.Information("""
188+
189+
190+
==== Demo for Jurisdiction ====
191+
192+
""");
193+
194+
// Creating different jurisdictions
195+
var district = Jurisdiction.District.Create("District 42");
196+
var country = Jurisdiction.Country.Create("DE");
197+
var unknown = Jurisdiction.Unknown.Instance;
198+
199+
// Comparing jurisdictions
200+
var district42 = Jurisdiction.District.Create("DISTRICT 42");
201+
logger.Information("district == district42: {IsEqual}", district == district42); // true
202+
203+
var district43 = Jurisdiction.District.Create("District 43");
204+
logger.Information("district == district43: {IsEqual}", district == district43); // false
205+
206+
logger.Information("unknown == Jurisdiction.Unknown.Instance: {IsEqual}", unknown == Jurisdiction.Unknown.Instance); // true
207+
208+
// Validation examples
209+
try
210+
{
211+
var invalidJuristiction = Jurisdiction.Country.Create("DEU"); // Throws ValidationException
212+
}
213+
catch (ValidationException ex)
214+
{
215+
logger.Information(ex.Message); // "ISO code must be exactly 2 characters long."
216+
}
217+
218+
var description = district.Switch(
219+
country: c => $"Country: {c}",
220+
federalState: s => $"Federal state: {s}",
221+
district: d => $"District: {d}",
222+
unknown: _ => "Unknown"
223+
);
224+
225+
logger.Information(description);
226+
}
180227
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace Thinktecture.Unions;
2+
3+
[Union]
4+
public abstract partial class Jurisdiction
5+
{
6+
[ValueObject<string>(KeyMemberName = "IsoCode")]
7+
[ValueObjectKeyMemberEqualityComparer<ComparerAccessors.StringOrdinalIgnoreCase, string>] // case-insensitive comparison
8+
[ValueObjectKeyMemberComparer<ComparerAccessors.StringOrdinalIgnoreCase, string>]
9+
public partial class Country : Jurisdiction
10+
{
11+
static partial void ValidateFactoryArguments(ref ValidationError? validationError, ref string isoCode)
12+
{
13+
if (string.IsNullOrWhiteSpace(isoCode))
14+
{
15+
validationError = new ValidationError("ISO code is required.");
16+
isoCode = string.Empty;
17+
return;
18+
}
19+
20+
isoCode = isoCode.Trim();
21+
22+
if (isoCode.Length != 2)
23+
validationError = new ValidationError("ISO code must be exactly 2 characters long.");
24+
}
25+
}
26+
27+
/// <summary>
28+
/// Let's assume that the federal state is represented by an number.
29+
/// </summary>
30+
[ValueObject<int>(KeyMemberName = "Number")]
31+
public partial class FederalState : Jurisdiction;
32+
33+
[ValueObject<string>(KeyMemberName = "Name")]
34+
[ValueObjectKeyMemberEqualityComparer<ComparerAccessors.StringOrdinalIgnoreCase, string>] // case-insensitive comparison
35+
[ValueObjectKeyMemberComparer<ComparerAccessors.StringOrdinalIgnoreCase, string>]
36+
public partial class District : Jurisdiction;
37+
38+
/// <summary>
39+
/// The complex type adds appropriate equality comparison(i.e. it checks for type only).
40+
/// </summary>
41+
[ComplexValueObject]
42+
public partial class Unknown : Jurisdiction
43+
{
44+
public static readonly Unknown Instance = new();
45+
}
46+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
5+
namespace Thinktecture.Unions;
6+
7+
8+
9+
public partial class JurisdictionJsonConverter : JsonConverter<Jurisdiction>
10+
{
11+
public override Jurisdiction? Read(
12+
ref Utf8JsonReader reader,
13+
Type typeToConvert,
14+
JsonSerializerOptions options)
15+
{
16+
if (!reader.Read() // read StartObject
17+
|| !reader.Read() // read PropertyName
18+
|| !Discriminator.TryGet(reader.GetString(), out var discriminator))
19+
throw new JsonException();
20+
21+
var jurisdiction = discriminator.ReadJurisdiction(ref reader, options);
22+
23+
if (!reader.Read()) // read EndObject
24+
throw new JsonException();
25+
26+
return jurisdiction;
27+
}
28+
29+
public override void Write(
30+
Utf8JsonWriter writer,
31+
Jurisdiction value,
32+
JsonSerializerOptions options)
33+
{
34+
value.Switch(
35+
(writer, options),
36+
country: static (state, country) =>
37+
WriteJurisdiction(state.writer, state.options, country, Discriminator.Country),
38+
federalState: static (state, federalState) =>
39+
WriteJurisdiction(state.writer, state.options, federalState, Discriminator.FederalState),
40+
district: static (state, district) =>
41+
WriteJurisdiction(state.writer, state.options, district, Discriminator.District),
42+
unknown: static (state, unknown) =>
43+
WriteJurisdiction(state.writer, state.options, unknown, Discriminator.Unknown)
44+
);
45+
}
46+
47+
private static void WriteJurisdiction<T>(
48+
Utf8JsonWriter writer,
49+
JsonSerializerOptions options,
50+
T jurisdiction,
51+
string discriminator
52+
)
53+
where T : Jurisdiction
54+
{
55+
writer.WriteStartObject();
56+
57+
writer.WriteString("$type", discriminator);
58+
59+
writer.WritePropertyName(options.PropertyNamingPolicy?.ConvertName("value") ?? "value");
60+
JsonSerializer.Serialize(writer, jurisdiction, options);
61+
62+
writer.WriteEndObject();
63+
}
64+
65+
[SmartEnum<string>]
66+
internal partial class Discriminator
67+
{
68+
public static readonly Discriminator Country = new("Country", ReadJurisdiction<Jurisdiction.Country>);
69+
public static readonly Discriminator FederalState = new("FederalState", ReadJurisdiction<Jurisdiction.FederalState>);
70+
public static readonly Discriminator District = new("District", ReadJurisdiction<Jurisdiction.District>);
71+
public static readonly Discriminator Unknown = new("Unknown", ReadJurisdiction<Jurisdiction.Unknown>);
72+
73+
[UseDelegateFromConstructor]
74+
public partial Jurisdiction? ReadJurisdiction(ref Utf8JsonReader reader, JsonSerializerOptions options);
75+
76+
private static Jurisdiction? ReadJurisdiction<T>(
77+
ref Utf8JsonReader reader,
78+
JsonSerializerOptions options)
79+
where T : Jurisdiction
80+
{
81+
if (!reader.Read() || !reader.Read()) // read PropertyName and value
82+
throw new JsonException();
83+
84+
return JsonSerializer.Deserialize<T>(ref reader, options);
85+
}
86+
}
87+
}

samples/Thinktecture.Runtime.Extensions.Samples/ValueObjects/EndDate.cs

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System;
2+
using System.Globalization;
3+
4+
namespace Thinktecture.ValueObjects;
5+
6+
[ValueObject<DateOnly>(SkipKeyMember = true, // We implement the key member "Date" ourselves
7+
KeyMemberName = nameof(Date), // Source Generator needs to know the name we've chosen
8+
DefaultInstancePropertyName = "Infinite", // "EndDate.Infinite" represent an open-ended end date
9+
EqualityComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads, // for comparison with DateOnly without implicit cast
10+
ComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
11+
AllowDefaultStructs = true,
12+
SkipToString = true)]
13+
public partial struct OpenEndDate
14+
{
15+
private readonly DateOnly? _date;
16+
17+
// can be public as well
18+
private DateOnly Date
19+
{
20+
get => _date ?? DateOnly.MaxValue;
21+
init => _date = value;
22+
}
23+
24+
/// <summary>
25+
/// Gets a value indicating whether the current date represents December 31st of any year.
26+
/// </summary>
27+
public bool IsEndOfYear => this != Infinite && Date is (_, 12, 31);
28+
29+
/// <summary>
30+
/// Creates an open-ended date with the specified year, month and day.
31+
/// </summary>
32+
public static OpenEndDate Create(int year, int month, int day)
33+
{
34+
return Create(new DateOnly(year, month, day));
35+
}
36+
37+
/// <summary>
38+
/// Creates an open-ended date from <see cref="DateTime"/>.
39+
/// </summary>
40+
public static OpenEndDate Create(DateTime dateTime)
41+
{
42+
return Create(dateTime.Year, dateTime.Month, dateTime.Day);
43+
}
44+
45+
static partial void ValidateFactoryArguments(
46+
ref ValidationError? validationError,
47+
ref DateOnly date
48+
)
49+
{
50+
if (date == DateOnly.MinValue)
51+
validationError = new ValidationError("The end date cannot be DateOnly.MinValue.");
52+
}
53+
54+
/// <summary>
55+
/// Adjusts the current date to the last day of the month.
56+
/// </summary>
57+
public OpenEndDate MoveToEndOfMonth()
58+
{
59+
if (this == Infinite)
60+
return this;
61+
62+
var days = DateTime.DaysInMonth(Date.Year, Date.Month);
63+
64+
return days == Date.Day
65+
? this
66+
: Create(Date.Year, Date.Month, days);
67+
}
68+
69+
/// <summary>
70+
/// Converts a nullable DateOnly to an open-ended date.
71+
/// </summary>
72+
public static explicit operator OpenEndDate(DateOnly? date) =>
73+
date is null ? Infinite : Create(date.Value);
74+
75+
public override string ToString() =>
76+
this == Infinite ? "Infinite" : Date.ToString("O", CultureInfo.InvariantCulture);
77+
}

0 commit comments

Comments
 (0)