Skip to content

Commit c149d44

Browse files
committed
Moved default comparer accessor to TT.Runtime.Extensions
1 parent 4fd156e commit c149d44

File tree

10 files changed

+217
-44
lines changed

10 files changed

+217
-44
lines changed

README.md

Lines changed: 137 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,148 @@ Features:
3838
* Provides appropriate constructor, based on the specified properties/fields
3939
* Provides means for lookup, cast and type conversion from key-type to Smart Enum and vice versa
4040
* Provides proper implementation of `Equals`, `GetHashCode`, `ToString` and equality comparison via `==` and `!=`
41+
* Provides implementation of `IComparable`, `IComparable<T>`, `IFormattable`, `IParsable<T>` and comparison operators `<`, `<=`, `>`, `>=` (if applicable to the underlying type)
4142
* [Choice between always-valid `IEnum<T>` and maybe-valid `IValidatableEnum<T>`](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#ienumt-vs-ivalidatableenumt)
43+
* [Makes use of abstract static members](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#make-use-of-abstract-static-members)
44+
* [Derived types can be generic](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#generic-derived-types)
4245
* [Allows custom validation of constructor arguments](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#validation-of-the-constructor-arguments)
4346
* [Allows changing the property name `Key`](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#changing-the-key-property-name), which holds the underlying value - thanks to [Roslyn Source Generator](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview)
4447
* [Allows custom key comparer](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#custom-key-comparer)
4548
* [JSON support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#json-serialization) (`System.Text.Json` and `Newtonsoft.Json`)
46-
* [ASP.NET Core support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#support-for-aspnet-core-model-binding) (model binding and model validation)
49+
* [Support for Minimal Web Api Parameter Binding and ASP.NET Core Model Binding](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#support-for-minimal-web-api-parameter-binding-and-aspnet-core-model-binding)
4750
* [Entity Framework Core support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#support-for-entity-framework-core) (`ValueConverter`)
4851
* [MessagePack support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#messagepack-serialization) (`IMessagePackFormatter`)
4952

50-
Definition of a new Smart Enum without any custom properties and methods. All other features mentioned above are generated by the [Roslyn Source Generators](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) in the background.
53+
Definition of a 2 Smart Enums without any custom properties and methods. All other features mentioned above are generated by the [Roslyn Source Generators](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) in the background.
5154

5255
```C#
53-
public partial class ProductType : IEnum<string>
56+
// Smart Enum with a string as the underlying type
57+
public sealed partial class ProductType : IEnum<string>
5458
{
5559
public static readonly ProductType Groceries = new("Groceries");
5660
public static readonly ProductType Housewares = new("Housewares");
5761
}
62+
63+
// Smart Enum with an int as the underlying type
64+
public sealed partial class ProductGroup : IEnum<int>
65+
{
66+
public static readonly ProductGroup Apple = new(1);
67+
public static readonly ProductGroup Orange = new(2);
68+
}
69+
```
70+
71+
Behind the scenes a Roslyn Source Generator, which comes with the library, generates additional code. Some of the features that are now available are ...
72+
73+
```C#
74+
// a private constructor which takes the key and additional members (if we had any)
75+
public sealed partial class ProductType : IEnum<string>
76+
{
77+
public static readonly ProductType Groceries = new("Groceries");
78+
...
79+
80+
------------
81+
82+
// a property for iteration over all items
83+
IReadOnlyList<ProductType> allTypes = ProductType.Items;
84+
85+
------------
86+
87+
// getting the item with specific name, i.e. its key
88+
// throw UnknownEnumIdentifierException if the provided key doesn't match to any item
89+
ProductType productType = ProductType.Get("Groceries");
90+
91+
// Alternatively, using an explicit cast (behaves the same as with Get)
92+
ProductType productType = (ProductType)"Groceries";
93+
94+
------------
95+
96+
// the same as above but returns a bool instead of throwing an exception (dictionary-style)
97+
bool found = ProductType.TryGet("Groceries", out ProductType productType);
98+
99+
------------
100+
101+
// similar to TryGet but returns a ValidationResult instead of a boolean.
102+
ValidationResult? validationResult = ProductType.Validate("Groceries", out productType);
103+
104+
if (validationResult == ValidationResult.Success)
105+
{
106+
logger.Information("Product type {Type} found with Validate", productType);
107+
}
108+
else
109+
{
110+
logger.Warning("Failed to fetch the product type with Validate. Validation result: {ValidationResult}", validationResult.ErrorMessage);
111+
}
112+
113+
------------
114+
115+
// implicit conversion to the type of the key
116+
string key = ProductType.Groceries; // "Groceries"
117+
118+
------------
119+
120+
// Equality comparison with 'Equals'
121+
// which compares the keys using default or custom 'IEqualityComparer<T>'
122+
bool equal = ProductType.Groceries.Equals(ProductType.Groceries);
123+
124+
------------
125+
126+
// Equality comparison with '==' and '!='
127+
bool equal = ProductType.Groceries == ProductType.Groceries;
128+
bool notEqual = ProductType.Groceries != ProductType.Groceries;
129+
130+
------------
131+
132+
// Hash code
133+
int hashCode = ProductType.Groceries.GetHashCode();
134+
135+
------------
136+
137+
// 'ToString' implementation
138+
string key = ProductType.Groceries.ToString(); // "Groceries"
139+
140+
------------
141+
142+
ILogger logger = ...;
143+
144+
// Switch-case (Action)
145+
productType.Switch(ProductType.Groceries, () => logger.Information("Switch with Action: Groceries"),
146+
ProductType.Housewares, () => logger.Information("Switch with Action: Housewares"));
147+
148+
// Switch-case with parameter (Action<TParam>) to prevent closures
149+
productType.Switch(logger,
150+
ProductType.Groceries, static l => l.Information("Switch with Action: Groceries"),
151+
ProductType.Housewares, static l => l.Information("Switch with Action: Housewares"));
152+
153+
// Switch case returning a value (Func<TResult>)
154+
var returnValue = productType.Switch(ProductType.Groceries, static () => "Switch with Func<T>: Groceries",
155+
ProductType.Housewares, static () => "Switch with Func<T>: Housewares");
156+
157+
// Switch case with parameter returning a value (Func<TParam, TResult>) to prevent closures
158+
returnValue = productType.Switch(logger,
159+
ProductType.Groceries, static l => "Switch with Func<T>: Groceries",
160+
ProductType.Housewares, static l => "Switch with Func<T>: Housewares");
161+
162+
------------
163+
164+
// Implements IParsable<T> which is especially helpful with minimal web apis.
165+
// This feature can be disabled if it doesn't make sense (see EnumGenerationAttribute).
166+
bool parsed = ProductType.TryParse("Groceries", null, out var parsedProductType);
167+
168+
------------
169+
170+
// Implements IFormattable if the underlyng type (like int) is an IFormattable itself.
171+
// This feature can be disabled if it doesn't make sense (see EnumGenerationAttribute).
172+
var formatted = ProductGroup.Apple.ToString("000", CultureInfo.InvariantCulture); // 001
173+
174+
------------
175+
176+
// Implements IComparable and IComparable<T> if the underlyng type (like int) is an IComparable itself.
177+
// This feature can be disabled if it doesn't make sense (see EnumGenerationAttribute).
178+
var comparison = ProductGroup.Apple.CompareTo(ProductGroup.Orange); // -1
179+
180+
// Implements comparison operators (<,<=,>,>=) if the underlyng type (like int) has comparison operators itself.
181+
// This feature can be disabled if it doesn't make sense (see EnumGenerationAttribute).
182+
var isBigger = ProductGroup.Apple > ProductGroup.Orange;
58183
```
59184

60185
Definition of a new Smart Enum with 1 custom property `RequiresFoodVendorLicense` and 1 method `Do` with different behaviors for different enum items.
@@ -91,24 +216,24 @@ public partial class ProductType : IEnum<string>
91216

92217
Install: `Install-Package Thinktecture.Runtime.Extensions`
93218

94-
Documentation: [Immutable Value Objects](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects)
219+
Documentation: [Value Objects](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects)
95220

96221
Features:
97222
* Roslyn Analyzers and CodeFixes help the developers to implement the Value Objects correctly
98223
* Allows custom properties and methods
99224
* Provides appropriate factory methods for creation of new value objects based on the specified properties/fields
100-
* Allows custom validation of [constructor](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects#validation-of-the-constructor-arguments) and [factory method](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects#validation-of-the-factory-method-arguments) arguments
101-
* Additional features for [simple Value Objects (1 "key"-property/field)](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects#simple-value-objects) and [complex Value Objects (2 properties/fields or more)](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects#complex-value-objects)
225+
* Allows custom validation of [constructor](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#validation-of-the-constructor-arguments) and [factory method](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#validation-of-the-factory-method-arguments) arguments
226+
* Additional features for [simple Value Objects (1 "key"-property/field)](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#simple-value-objects) and [complex Value Objects (2 properties/fields or more)](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#complex-value-objects)
102227
* Simple Value Objects: allows cast and type conversion from key-type to Value Object and vice versa
103228
* Simple Value Objects: provides an implementation of `IComparable<T>` if the key-property/field is an `IComparable<T>` or has an `IComparer<T>`
104229
* Simple Value Objects: provides an implementation of `IFormattable` if the key-property/field is an `IFormattable`
105230
* Provides proper implementation of `Equals`, `GetHashCode`, `ToString` and equality comparison via `==` and `!=`
106-
* [Allows custom equality comparison](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects#custom-comparer)
107-
* Handling of [null](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects#null-in-factory-methods-yields-null) and [empty strings](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects#empty-string-in-factory-methods-yields-null)
108-
* [JSON support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects#json-serialization) (`System.Text.Json` and `Newtonsoft.Json`)
109-
* [ASP.NET Core support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects#support-for-aspnet-core-model-binding) (model binding and model validation)
110-
* [Entity Framework Core support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects#support-for-entity-framework-core) (`ValueConverter`)
111-
* [MessagePack support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Immutable-Value-Objects#messagepack-serialization) (`IMessagePackFormatter`)
231+
* [Allows custom equality comparison](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#custom-comparer)
232+
* Handling of [null](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#null-in-factory-methods-yields-null) and [empty strings](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#empty-string-in-factory-methods-yields-null)
233+
* [JSON support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#json-serialization) (`System.Text.Json` and `Newtonsoft.Json`)
234+
* [ASP.NET Core support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#support-for-aspnet-core-model-binding) (model binding and model validation)
235+
* [Entity Framework Core support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#support-for-entity-framework-core) (`ValueConverter`)
236+
* [MessagePack support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#messagepack-serialization) (`IMessagePackFormatter`)
112237

113238
Definition of a value object with 1 custom property `Value`. All other features mentioned above are generated by the [Roslyn Source Generators](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) in the background.
114239

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.EntityFrameworkCore;
2+
using Thinktecture.SmartEnums;
23

34
namespace Thinktecture;
45

@@ -26,9 +27,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
2627
});
2728

2829
// Alternative way to apply ValueConverters to Smart Enums and Value Objects
29-
// modelBuilder.AddEnumAndValueObjectConverters(true, property =>
30-
// {
31-
//
32-
// });
30+
// modelBuilder.AddEnumAndValueObjectConverters(validateOnWrite: true,
31+
// configureEnumsAndKeyedValueObjects: property =>
32+
// {
33+
// if (property.ClrType == typeof(ProductType))
34+
// property.SetMaxLength(20);
35+
// });
3336
}
3437
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ private static IServiceProvider CreateServiceProvider(LoggingLevelSwitch logging
8585

8686
builder.AddSerilog(serilogLogger);
8787
})
88-
.AddDbContext<ProductsDbContext>(builder => builder.UseSqlServer("Server=localhost;Database=TT-Runtime-Extensions-Demo;Integrated Security=true")
88+
.AddDbContext<ProductsDbContext>(builder => builder.UseSqlServer("Server=localhost;Database=TT-Runtime-Extensions-Demo;Integrated Security=true;TrustServerCertificate=true")
8989
.EnableSensitiveDataLogging()
9090
.UseValueObjectValueConverter(configureEnumsAndKeyedValueObjects: property =>
9191
{

samples/Thinktecture.Runtime.Extensions.Samples/SmartEnums/SmartEnumDemos.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ private static void DemoForNonValidatableEnum(ILogger logger)
6868

6969
logger.Information(returnValue);
7070

71+
var parsed = ProductType.TryParse("Groceries", null, out var parsedProductType);
72+
logger.Information("Parsed: {Parsed}", parsedProductType);
73+
7174
var formatted = ProductGroup.Apple.ToString("000", CultureInfo.InvariantCulture); // 001
7275
logger.Information("Formatted: {Formatted}", formatted);
7376

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ static partial void ValidateFactoryArguments(ref ValidationResult? validationRes
1313
if (lower <= upper)
1414
return;
1515

16-
validationResult = new ValidationResult($"Lower boundary '{lower}' must be less than upper boundary '{upper}'");
16+
validationResult = new ValidationResult($"Lower boundary '{lower}' must be less than upper boundary '{upper}'",
17+
new[] { nameof(Lower), nameof(Upper) });
1718
}
1819
}

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

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.ComponentModel.DataAnnotations;
2+
using System.Globalization;
23
using Serilog;
34

45
namespace Thinktecture.ValueObjects;
@@ -7,7 +8,13 @@ public class ValueObjectDemos
78
{
89
public static void Demo(ILogger logger)
910
{
10-
logger.Information("==== Demo for ValueObjectAttribute ====");
11+
DemoForSimpleValueObjects(logger);
12+
DemoForComplexValueObjects(logger);
13+
}
14+
15+
private static void DemoForSimpleValueObjects(ILogger logger)
16+
{
17+
logger.Information("==== Demo for Simple Value Objects ====");
1118

1219
var bread = ProductName.Create("Bread");
1320
logger.Information("Product name: {Bread}", bread);
@@ -51,12 +58,47 @@ public static void Demo(ILogger logger)
5158
if (ProductName.TryCreate(null, out nullProduct))
5259
logger.Information("Null-Product name: {NullProduct}", nullProduct);
5360

54-
// Addition / subtraction / multiplication / division / comparison
61+
if (ProductName.TryParse("New product name", null, out var productName))
62+
logger.Information("Parsed name: {ParsedProductName}", productName);
63+
64+
var formattedValue = Amount.Create(42).ToString("000", CultureInfo.InvariantCulture); // "042"
65+
logger.Information("Formatted: {Formatted}", formattedValue);
66+
5567
var amount = Amount.Create(1);
5668
var otherAmount = Amount.Create(2);
69+
70+
var comparison = amount.CompareTo(otherAmount);
71+
logger.Information("Comparison: {Comparison}", comparison);
72+
73+
// Addition / subtraction / multiplication / division / comparison
5774
var sum = amount + otherAmount;
5875
logger.Information("{Amount} + {Other} = {Sum}", amount, otherAmount, sum);
5976

6077
logger.Information("{Amount} > {Other} = {Result}", amount, otherAmount, amount > otherAmount);
6178
}
79+
80+
private static void DemoForComplexValueObjects(ILogger logger)
81+
{
82+
logger.Information("==== Demo for Complex Value Objects ====");
83+
84+
Boundary boundaryWithCreate = Boundary.Create(lower: 1, upper: 2);
85+
logger.Information("Boundary with Create: {Boundary}", boundaryWithCreate);
86+
87+
if (Boundary.TryCreate(lower: 1, upper: 2, out var boundaryWithTryCreate))
88+
logger.Information("Boundary with TryCreate: {Boundary}", boundaryWithTryCreate);
89+
90+
var validationResult = Boundary.Validate(lower: 1, upper: 2, out var boundaryWithValidate);
91+
92+
if (validationResult == ValidationResult.Success)
93+
{
94+
logger.Information("Boundary {Boundary} created via Validate", boundaryWithValidate);
95+
}
96+
else
97+
{
98+
logger.Warning("Failed to create boundary. Validation result: {ValidationResult}", validationResult!.ErrorMessage);
99+
}
100+
101+
var equal = boundaryWithCreate.Equals(boundaryWithCreate);
102+
logger.Information("Boundaries are equal: {Equal}", equal);
103+
}
62104
}

src/Thinktecture.Runtime.Extensions/ComparerAccessors.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,17 @@ public class StringOrdinalIgnoreCase : IEqualityComparerAccessor<string>, ICompa
2828
/// <inheritdoc />
2929
public static IComparer<string> Comparer => StringComparer.OrdinalIgnoreCase;
3030
}
31+
32+
/// <summary>
33+
/// Provides the default comparers.
34+
/// </summary>
35+
/// <typeparam name="T"></typeparam>
36+
public class Default<T> : IEqualityComparerAccessor<T>, IComparerAccessor<T>
37+
{
38+
/// <inheritdoc />
39+
public static IEqualityComparer<T> EqualityComparer => EqualityComparer<T>.Default;
40+
41+
/// <inheritdoc />
42+
public static IComparer<T> Comparer => Comparer<T>.Default;
43+
}
3144
}

0 commit comments

Comments
 (0)