Skip to content

Commit

Permalink
Merge pull request #580 from SteveDunn/571-guid-factory-method
Browse files Browse the repository at this point in the history
Add factory method for GUIDs
  • Loading branch information
SteveDunn authored Apr 29, 2024
2 parents c701f3e + 818fb5b commit 60c8037
Show file tree
Hide file tree
Showing 39 changed files with 10,097 additions and 34 deletions.
8 changes: 4 additions & 4 deletions docs/site/Writerside/topics/how-to/Testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ So the tests are in two solutions:

* In the **main solution**, there are [snapshot tests](https://github.com/VerifyTests/Verify) which create **in-memory projects** that exercise the generators using different versions of the .NET Framework.
These are slow to run because they use many different permutations of features and dozens of variations of configuration/primitive-type/C#-type/accessibility-type/converters, for example:
* does it correctly generate a record struct with instances and normalization and a type converter and a Dapper serializer
* does it correctly generate a class with no converters, no validation, and no serialization
* does it correctly generate a readonly struct with a LinqToDb converter
* does it correctly generate a record struct with instances and normalization and a type converter and a Dapper serializer?
* does it correctly generate a class with no converters, no validation, and no serialization?
* does it correctly generate a readonly struct with a LinqToDb converter?
* etc. etc.

(all tests run for each supported framework)
Expand All @@ -30,7 +30,7 @@ The snapshot tests in the IDE run in about 5 minutes. In the CI build, we set a
These tests are much quicker to run.
They verify the behavior
of created Value Objects, such as:
* [Normalization](https://github.com/SteveDunn/Vogen/wiki/Normalization)
* [Normalization](NormalizationHowTo.md "How to use normalization")
* Equality
* Hashing
* ToString
Expand Down
15 changes: 8 additions & 7 deletions docs/site/Writerside/topics/reference/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,23 @@ This topic is incomplete and is currently being improved.
Each Value Object can have its own *optional* configuration. Configuration includes:

* The underlying type
* Any 'conversions' (Dapper, System.Text.Json, Newtonsoft.Json, etc.) - see [the Integrations page](https://github.com/SteveDunn/Vogen/wiki/Integration) in the wiki for more information
* Any 'conversions' (Dapper, System.Text.Json, Newtonsoft.Json, etc.) - see Integrations in the wiki for more information
* The type of the exception that is thrown when validation fails

If any of those above are not specified, then global configuration is inferred. It looks like this:

```c#
[assembly: VogenDefaults(underlyingType: typeof(int), conversions: Conversions.Default, throws: typeof(ValueObjectValidationException))]
[assembly: VogenDefaults(
underlyingType: typeof(int),
conversions: Conversions.Default,
throws: typeof(ValueObjectValidationException))]
```

Those again are optional. If they're not specified, then they are defaulted to:

* Underlying type = `typeof(int)`
* Conversions = `Conversions.Default` (`TypeConverter` and `System.Text.Json`)
* Validation exception type = `typeof(ValueObjectValidationException)`
* Conversions = `Conversions.Default` which is `TypeConverter` and `System.Text.Json`
* Validation exception type = which is `ValueObjectValidationException`

There are several code analysis warnings for invalid configuration, including:
Several code analysis warnings exist for invalid configuration, including:

* when you specify an exception that does not derive from `System.Exception`
* when your exception does not have one public constructor that takes an int
Expand Down
33 changes: 21 additions & 12 deletions docs/site/Writerside/topics/reference/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ correct.
## How do I identify types that are generated by Vogen?
_I'd like to be able to identify types that are generated by Vogen so that I can integrate them in things like EFCore._

**A: ** You can use this information on [this page](How-to-identify-a-type-that-is-generated-by-Vogen.md)

This is described in [this how-to page](How-to-identify-a-type-that-is-generated-by-Vogen.md)

### What versions of .NET are supported?

Expand Down Expand Up @@ -325,20 +324,14 @@ Yes, add NormalizeInput method, e.g.
```c#
private static string NormalizeInput(string input) => input.Trim();
```
See [wiki](https://github.com/SteveDunn/Vogen/wiki/Normalization) for more information.
See [the how-to page](NormalizationHowTo.md) for more information.


### Can I create custom Value Object attributes with my own defaults?

Yes, but (at the moment) it requires that you put your defaults in your attribute's constructor—not in the call to
the base class' constructor (see [this comment](https://github.com/SteveDunn/Vogen/pull/321#issuecomment-1399324832)).

```c#
public class CustomValueObjectAttribute : ValueObjectAttribute<long>
{
// This attribute will default to having both the default conversions and EF Core type conversions
public CustomValueObjectAttribute(Conversions conversions = Conversions.Default | Conversions.EfCoreValueConverter) { }
}
No. It used to be possible, but it impacts the performance of Vogen.
A much better way is
to use [type alias feature C# 12](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/using-alias-types).
```
NOTE: *custom attributes must extend a ValueObjectAttribute class; you cannot layer custom attributes on top of each other*
Expand Down Expand Up @@ -470,3 +463,19 @@ The BoxId type will now be serialized as a `string` in all messages and grpc cal
for other applications from C#, proto files will include the `Surrogate` type as the type.

_thank you to [@DomasM](https://github.com/DomasM) for this information_.

### Can I have a factory method for value objects that wrap GUIDs?

Yes, use the `Customizations.AddFactoryMethodForGuids` in the global config attribute, e.g.

```c#
[assembly: VogenDefaults(
customizations: Customizations.AddFactoryMethodForGuids)]

[ValueObject<Guid>]
public partial {{type}} CustomerId { }

...

var newCustomerId = CustomerId.FromNewGuid();
```
13 changes: 9 additions & 4 deletions src/Vogen.SharedTypes/Customizations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
namespace Vogen;

/// <summary>
/// Customization flags. For things like treating doubles as strings
/// during [de]serialization (for compatibility with JavaScript).
/// Customization flags. For simple binary choices.
/// More complex configuration options are specified as parameters in the <see cref="VogenDefaultsAttribute"/>.
/// </summary>
[Flags]
public enum Customizations
Expand All @@ -17,10 +17,15 @@ public enum Customizations
None = 0,

/// <summary>
/// When [de]serializing an underlying primitive that wold normally be written as a number in System.Text.Json,
/// When [de]serializing an underlying primitive that would normally be written as a number in System.Text.Json,
/// instead, treat the underlying primitive as a culture invariant string. This gets around the issue of
/// JavaScript losing precision on very large numbers. See <see href="https://github.com/SteveDunn/Vogen/issues/165"/>
/// for more information.
/// </summary>
TreatNumberAsStringInSystemTextJson = 1 << 0
TreatNumberAsStringInSystemTextJson = 1 << 0,

/// <summary>
/// For GUIDs, add a `FromNewGuid()` factory method, which is just `public static MyVo FromNewGuid() => From(Guid.NewGuid());`
/// </summary>
AddFactoryMethodForGuids = 1 << 1
}
2 changes: 1 addition & 1 deletion src/Vogen/Generators/ClassGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public string BuildClass(VoWorkItem item, TypeDeclarationSyntax tds)
public static global::System.Boolean operator ==({itemUnderlyingType} left, {className} right) => Equals(left, right.Value);
public static global::System.Boolean operator !=({itemUnderlyingType} left, {className} right) => !Equals(left, right.Value);
{GenerateCastingOperators.Generate(item,tds)}
{GenerateCastingOperators.Generate(item,tds)}{Util.GenerateGuidFactoryMethodIfRequired(item, tds)}
{GenerateComparableCode.GenerateIComparableImplementationIfNeeded(item, tds)}
{GenerateCodeForTryParse.GenerateAnyHoistedTryParseMethods(item)}{GenerateCodeForParse.GenerateAnyHoistedParseMethods(item)}
Expand Down
2 changes: 1 addition & 1 deletion src/Vogen/Generators/RecordClassGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public string BuildClass(VoWorkItem item, TypeDeclarationSyntax tds)
public static global::System.Boolean operator ==({itemUnderlyingType} left, {className} right) => Equals(left, right.Value);
public static global::System.Boolean operator !=({itemUnderlyingType} left, {className} right) => !Equals(left, right.Value);
{GenerateCastingOperators.Generate(item,tds)}
{GenerateCastingOperators.Generate(item,tds)}{Util.GenerateGuidFactoryMethodIfRequired(item, tds)}
{GenerateComparableCode.GenerateIComparableImplementationIfNeeded(item, tds)}
{GenerateCodeForTryParse.GenerateAnyHoistedTryParseMethods(item)}{GenerateCodeForParse.GenerateAnyHoistedParseMethods(item)}
Expand Down
2 changes: 1 addition & 1 deletion src/Vogen/Generators/RecordStructGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public readonly {itemUnderlyingType} Value
return instance;
}}
{GenerateEqualsAndHashCodes.GenerateStringComparersIfNeeded(item, tds)}
{GenerateCastingOperators.Generate(item,tds)}
{GenerateCastingOperators.Generate(item,tds)}{Util.GenerateGuidFactoryMethodIfRequired(item, tds)}
// only called internally when something has been deserialized into
// its primitive type.
private static {structName} Deserialize({itemUnderlyingType} value)
Expand Down
2 changes: 1 addition & 1 deletion src/Vogen/Generators/StructGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public readonly {itemUnderlyingType} Value
}}
{GenerateEqualsAndHashCodes.GenerateStringComparersIfNeeded(item, tds)}
{GenerateCastingOperators.Generate(item,tds)}
{GenerateCastingOperators.Generate(item,tds)}{Util.GenerateGuidFactoryMethodIfRequired(item, tds)}
// only called internally when something has been deserialized into
// its primitive type.
private static {structName} Deserialize({itemUnderlyingType} value)
Expand Down
10 changes: 10 additions & 0 deletions src/Vogen/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,16 @@ public static string GenerateToStringReadOnly(VoWorkItem item) =>
$@"/// <summary>Returns the string representation of the underlying type</summary>
/// <inheritdoc cref=""{item.UnderlyingTypeFullName}.ToString()"" />
public readonly override global::System.String ToString() =>_isInitialized ? Value.ToString() : ""[UNINITIALIZED]"";";

public static string GenerateGuidFactoryMethodIfRequired(VoWorkItem item, TypeDeclarationSyntax tds)
{
if (item.UnderlyingTypeFullName == "System.Guid" && item.Customizations.HasFlag(Customizations.AddFactoryMethodForGuids))
{
return $"public static {item.VoTypeName} FromNewGuid() {{ return From(global::System.Guid.NewGuid()); }}";
}

return string.Empty;
}
}

public static class DebugGeneration
Expand Down
28 changes: 25 additions & 3 deletions tests/SnapshotTests/GeneralStuff/GeneralTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,31 @@ namespace SnapshotTests.GeneralStuff;
[UsesVerify]
public class GeneralTests
{
[Theory]
[InlineData("struct")]
[InlineData("class")]
[InlineData("record struct")]
[InlineData("record class")]
public async Task Can_specify_a_factory_method_for_wrappers_for_guids(string type)
{
var source = $$"""

using System;
using Vogen;

[assembly: VogenDefaults(customizations: Customizations.AddFactoryMethodForGuids)]

[ValueObject<Guid>]
public partial {{type}} MyVo { }

""";

await new SnapshotRunner<ValueObjectGenerator>()
.WithSource(source)
.CustomizeSettings(s => s.UseFileName(TestHelper.ShortenForFilename(type)))
.RunOnAllFrameworks();
}

[Fact]
public async Task ServiceStackDotTextConversion_generates_static_constructor_for_strings()
{
Expand All @@ -21,7 +46,6 @@ public partial struct MyVo { }
static Task RunTest(string source) =>
new SnapshotRunner<ValueObjectGenerator>()
.WithSource(source)
//.WithPackage(new NuGetPackage("ServiceStack.Text", "8.2.2", "lib/net8.0" ))
.RunOn(TargetFramework.Net8_0);
}

Expand Down Expand Up @@ -56,7 +80,6 @@ public partial struct MyVo { }
static Task RunTest(string source) =>
new SnapshotRunner<ValueObjectGenerator>()
.WithSource(source)
// .WithPackage(new NuGetPackage("ServiceStack.Text", "8.2.2", "lib/net8.0" ))
.RunOn(TargetFramework.Net8_0);
}

Expand All @@ -74,7 +97,6 @@ public partial struct MyVo { }
static Task RunTest(string source) =>
new SnapshotRunner<ValueObjectGenerator>()
.WithSource(source)
// .WithPackage(new NuGetPackage("ServiceStack.Text", "8.2.2", "lib/net8.0" ))
.RunOn(TargetFramework.Net8_0);
}

Expand Down
Loading

0 comments on commit 60c8037

Please sign in to comment.