Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add factory method for GUIDs #580

Merged
merged 2 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading