Skip to content

Commit

Permalink
Merge pull request #683 from SteveDunn/primitive-equality-in-interface
Browse files Browse the repository at this point in the history
Primitive equality in interface
  • Loading branch information
SteveDunn authored Oct 20, 2024
2 parents 09805e8 + 8d141a9 commit 57d1d84
Show file tree
Hide file tree
Showing 13,278 changed files with 157,848 additions and 225,752 deletions.
The diff you're trying to view is too large. We only load the first 3000 changed files.
92 changes: 66 additions & 26 deletions docs/site/Writerside/topics/reference/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ To run the snapshot tests, run `RunSnapshots.ps1`
## I want to add a test, where is the best place for it?

Most tests involve checking two, maybe three, things:
* checking that source generated is as expected (so the snapshot tests in the main solution)
* checking that behavior of the change works as expected (so a test in the `consumers` solution (`tests/consumers.sln`))
* (maybe) - if you added/changed behavior of the code that generates the source code (rather than just changing a template), then a unit test in the main solution
* checking that the source generated is as expected (so the snapshot tests in the main solution)
* checking that the behavior of the change works as expected (so a test in the `consumers` solution (`tests/consumers.sln`))
* (maybe) - if you added/changed the behavior of the code that generates the source code (rather than just changing a template), then a unit test in the main solution

## I've changed the source that is generated, and I now have snapshot failures, what should I do?

Expand All @@ -30,11 +30,9 @@ If your change modifies what is generated, then it is likely that a lot of `veri
the new source code that is generated.

To do this, run `RunSnapshots.ps1 -reset`. This will delete all the snapshots and treat what is generated
as the correct version. Needless to say, only do this if you're sure that the newly generated code is
as the correct version. Needless to say, only do this if you're sure the newly generated code is
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._

Expand All @@ -46,7 +44,7 @@ The source generator is .NET Standard 2.0. The code it generates supports all C#

If you're using the generator in a .NET Framework project and using the old style projects (the one before the 'SDK style' projects), then you'll need to do a few things differently:

* add the reference using `PackageReference` in the .csproj file:
* add the reference using `PackageReference` in the `.csproj` file:

```xml
<ItemGroup>
Expand All @@ -67,7 +65,7 @@ If you're using the generator in a .NET Framework project and using the old styl
```

## Does it support C# 11 features?
This is primarily a source generator. The source it generates is mostly C# 6 for compatibility. But if you use features from a later language version, for instance `records` from C# 9, then it will also generate records.
This is primarily a source generator. The source it generates is mostly C# 6 for compatibility. But if you use features from a later language version, for instance, `records` from C# 9, then it will also generate records.

Source generation is driven by attributes, and, if you're using .NET 7 or above, the generic version of the `ValueObject` attribute is exposed:

Expand All @@ -84,7 +82,7 @@ The term Value Object represents a small object whose equality is based on value
In DDD, a Value Object is (again, from [Wikipedia](https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks))

> _... a Value Object is an immutable object that contains attributes but has no conceptual identity_
> _ a Value Object is an immutable object that contains attributes but has no conceptual identity_
## How can I view the code that is generated?

Expand Down Expand Up @@ -126,7 +124,7 @@ public record struct CustomerId
```

You might also provide other constructors which might not validate the data, thereby _allowing invalid data
into your domain_. Those other constructors might not throw exception, or might throw different exceptions.
into your domain_. Those other constructors might not throw exceptions or might throw different exceptions.
One of the opinions in Vogen is that any invalid data given to a Value Object throws a `ValueObjectValidationException`.

You could also use `default(CustomerId)` to evade validation. In Vogen, there are analyzers that catch this and fail the build, e.g.:
Expand Down Expand Up @@ -241,7 +239,7 @@ There are also issues with validation that [violates the rules of implicit opera
But a primitive cast to a value object might not always succeed due to validation.

In my research, I read some other opinions, and noted that the guidelines listed in [this answer](https://softwareengineering.stackexchange.com/a/284377/30906) say:
In my research, I read some other opinions and noted that the guidelines listed in [this answer](https://softwareengineering.stackexchange.com/a/284377/30906) say:

* If the conversion can throw an `InvalidCast` exception, then it shouldn't be implicit.
* If the conversion causes a heap allocation each time it is performed, then it shouldn't be implicit.
Expand All @@ -256,7 +254,7 @@ Yes, by specifying the `toPrimitiveCasting` and `fromPrimitiveCasting` in either
By default, explicit operators are generated for both. Bear in mind that you can only define implicit _or_ explicit operators;
you can't have both.

Also, bear in mind that ease of use can cause confusion. Let's say there's a type like this (and imagine that there's implicit conversions to `Age` and to `int`):
Also, bear in mind that ease of use can cause confusion. Let's say there's a type like this (and imagine that there are implicit conversions to `Age` and to `int`):

```c#
[ValueObject(typeof(int))]
Expand Down Expand Up @@ -296,7 +294,7 @@ public void SomSomething(
ProductId productId);
```

... but with the interface, we _could_ have signatures such as this:
but with the interface, we _could_ have signatures such as this:

```c#
public void SomSomething(
Expand All @@ -309,7 +307,7 @@ So, callers could mess things up by calling `DoSomething(productId, supplierId,

There would also be no need to know if it's validated, as, if it's in your domain, **it's valid** (there's no way to manually create invalid instances). And with that said, there would also be no point in exposing the 'Validate' method via the interface because validation is done at creation.

Having said that, outside of the domain, it can be useful to have an interface. The [Guids tutorial](how-to-guides.topic) describes how to generate the interface and when it's useful to do so.
Having said that, outside the domain, it can be useful to have an interface. The [Guids tutorial](how-to-guides.topic) describes how to generate the interface and when it's useful to do so.

## Can I represent special values

Expand All @@ -334,7 +332,7 @@ public readonly partial struct Age
}
```

Using `new` is only permitted in the value object itself, and bypassed validation (and normalization).
Using `new` is only permitted in the value object itself and bypassed validation (and normalization).

You can then use default values when using these types, e.g.

Expand All @@ -344,7 +342,7 @@ public class Person {
}
```

... and if you take an Age, you can compare it to an instance that is invalid/unspecified
and if you take an Age, you can compare it to an instance that is invalid/unspecified

```c#
public void CanEnter(Age age) {
Expand All @@ -369,17 +367,17 @@ See [the how-to page](NormalizationHowTo.md) for more information.

No, it used to be possible, but it impacts the performance of Vogen.
A much better way is
to use [type alias feature](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive#using-alias).
to use [the type alias feature](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive#using-alias).

NOTE: *custom attributes must extend a ValueObjectAttribute class; you cannot layer custom attributes on top of each other*
NOTE: *custom attributes must extend a ValueObjectAttribute class; you can’t layer custom attributes on top of each other*

## Why isn't this concept part of the C# language?

It would be great if it was, but it's not there currently. I [wrote an article about it](https://dunnhq.com/posts/2022/non-defaultable-value-types/), but in summary, there is a [long-standing language proposal](https://github.com/dotnet/csharplang/issues/146) focusing on non-defaultable value types.
Having non-defaultable value types is a great first step, but it would also be handy to have something in the language to enforce validation.
So I added a [language proposal for invariant records](https://github.com/dotnet/csharplang/discussions/5574).

One of the responses in the proposal says that the language team decided that validation policies should not be part of C#, but provided by source generators.
One of the responses in the proposal says that the language team decided that validation policies shouldn’t be part of C# but provided by source generators.

## How do I run the benchmarks?

Expand All @@ -405,7 +403,7 @@ This is focused more on IDs. Vogen is focused more on 'Domain Concepts' and the
This is my first attempt and is NON-source-generated. There is memory overhead because the base type is a class. There are also no analyzers. It is now marked as deprecated in favor of Vogen.

[ValueOf](https://github.com/mcintyre321/ValueOf)
Similar to StringlyTyped - non-source-generated and no analyzers. This is also more relaxed and allows composite 'underlying' types.
Like StringlyTyped - non-source-generated and no analyzers. This is also more relaxed and allows composite 'underlying' types.

[ValueObjectGenerator](https://github.com/RyotaMurohoshi/ValueObjectGenerator)
Similar to Vogen, but less focused on validation and no code analyzer.
Expand Down Expand Up @@ -443,8 +441,8 @@ conversion and serialization, then specify `None` for converters and decorate yo
public partial struct SpecialMeasurement;
```

Collections are not allowed as they don't exhibit value-equality.
It is worth considering if the type you are wrapping in your value object exhibits value-equality.
Collections aren’t allowed as they don't exhibit value-equality.
It is worth considering if the type you're wrapping in your value object exhibits value-equality.
For types that don't, it completely breaks down the principal of value objects.

## I've made a change that means the 'Snapshot' tests are expectedly failing in the build—what do I do?
Expand All @@ -453,7 +451,7 @@ Vogen uses a combination of unit tests, in-memory compilation tests, and snapsho
to compare the output of the source generators to the expected output stored on disk.

If your feature/fix changes the output of the source generators, then running the snapshot tests will bring up your
configured code diff tool (for example, Beyond Compare), to show the differences. You can accept the differences in that
configured code diff tool (for example, Beyond Compare) to show the differences. You can accept the differences in that
tool, or, if there are lots of differences (and they're all expected!), you have various options depending on your
platform and tooling. Those are [described here](https://github.com/VerifyTests/Verify/blob/main/docs/clipboard.md).

Expand All @@ -469,7 +467,7 @@ but it's expected and unavoidable.

## How do I debug the source generator?

The easiest way is to debug the SnapshotTests. Put a breakpoint in the code, and then debug a test somewhere.
The easiest way is to debug the SnapshotTests. Put a breakpoint in the code and then debug a test somewhere.

To debug an analyzer, select or write a test in the AnalyzerTests. There are tests that exercise the various analyzers and code-fixers.

Expand All @@ -482,7 +480,7 @@ references the latest version. The consumer can then _really_ use the source gen

## Can I get it to throw my own exception?

Yes, by specifying the exception type in either the `ValueObject` attribute, or globally, with `VogenConfiguration`.
Yes, by specifying the exception type in either the `ValueObject` attribute or, globally, with `VogenConfiguration`.

## I get an error from Linq2DB when I use a ValueObject that wraps a `TimeOnly` saying that `DateTime` cannot be converted to `TimeOnly`—what should I do?

Expand Down Expand Up @@ -551,7 +549,7 @@ There are two solutions:
They are by default, but they can be configured so that they're not.

They're bigger because Vogen generates code that stores a field named `_isInitialized`. This
is used to check that the instance is initialized, e.g. after deserializing.
is used to check that the instance is initialized, e.g., after deserializing.
If you don't want that, then you can specify in your project that you don't want validation, e.g.

```xml
Expand Down Expand Up @@ -581,3 +579,45 @@ If it is important that your debug builds have the same size value objects as yo
[assembly: VogenDefaults(disableStackTraceRecordingInDebug: true)]
```

## Is it possible to see any diagnostics from the source generator?

Yes, it's possible to see some diagnostic information. Vogen looks for a 'marker type' and if present, generates a file with diagnostic information. Include the following marker type in your project:

```c#
namespace Vogen
{
public class __ProduceDiagnostics;
}
```

This will produce a `diagnostics.cs` file which you can see in your IDE's solution tree or (in Windows) in a folder something like:
`C:\Users\[user]\AppData\Local\Temp\SourceGeneratedDocuments\[3F61CDF56DC2FDAC8E2336AD]\Vogen\Vogen.ValueObjectGenerator`

The file looks similar to this:

```c#
/*
Generator count: 20
LanguageVersion: CSharp12
User provided global config
===========
UnderlyingType: null
ValidationExceptionType: null
[... etc etc.]
Resolved global config
===========
UnderlyingType: null
ValidationExceptionType: null
[... etc etc.]
Targets
===========
CustomerId
SomethingElseId
SupplierId
*/
```
7 changes: 7 additions & 0 deletions samples/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)..\'))" />
<PropertyGroup>
<LangVersion>preview</LangVersion>
<NoWarn>$(NoWarn);NU1903</NoWarn>
</PropertyGroup>
</Project>
9 changes: 9 additions & 0 deletions src/Vogen/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ public static string EscapeTypeNameForTripleSlashComment(string typeName) =>

public static string SanitizeToALegalFilename(string input) => input.Replace('@', '_');

public static SourceText FormatSource(string source)
{
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source);
SyntaxNode root = syntaxTree.GetRoot();
SyntaxNode formatted = root.NormalizeWhitespace();

return SourceText.From(formatted.ToFullString(), Encoding.UTF8);
}

public static void TryWriteUsingUniqueFilename(string filename, SourceProductionContext context, SourceText sourceText)
{
int count = 0;
Expand Down
2 changes: 1 addition & 1 deletion src/Vogen/WriteOpenApiSchemaCustomizationCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ private static void TryCopyPublicProperties<T>(T oldObject, T newObject) where T
}
""";

context.AddSource("SwashbuckleSchemaFilter_g.cs", source);
context.AddSource("SwashbuckleSchemaFilter_g.cs", Util.FormatSource(source));
}

private static void WriteExtensionMethodMapping(SourceProductionContext context,
Expand Down
27 changes: 19 additions & 8 deletions src/Vogen/WriteStaticAbstracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static void WriteInterfacesAndMethodsIfNeeded(VogenConfiguration? globalC
""";
}

context.AddSource("VogenInterfaces_g.cs", source);
context.AddSource("VogenInterfaces_g.cs", Util.FormatSource(source));

string GenerateSource()
{
Expand Down Expand Up @@ -124,18 +124,29 @@ string GenerateEqualsOperatorsIfNeeded()
{
return string.Empty;
}

return """
string s = """
static abstract bool operator ==(TSelf left, TSelf right);
static abstract bool operator !=(TSelf left, TSelf right);
""";

static abstract bool operator ==(TSelf left, TPrimitive right);
static abstract bool operator !=(TSelf left, TPrimitive right);
var primitiveOperatorGeneration = globalConfig?.PrimitiveEqualityGeneration ??
VogenConfiguration.DefaultInstance.PrimitiveEqualityGeneration;

static abstract bool operator ==(TPrimitive left, TSelf right);
static abstract bool operator !=(TPrimitive left, TSelf right);
if (primitiveOperatorGeneration.HasFlag(PrimitiveEqualityGeneration.GenerateOperators))
{
s = s + """
""";
static abstract bool operator ==(TSelf left, TPrimitive right);
static abstract bool operator !=(TSelf left, TPrimitive right);
static abstract bool operator ==(TPrimitive left, TSelf right);
static abstract bool operator !=(TPrimitive left, TSelf right);
""";
}

return s;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Vogen/WriteSystemTextJsonConverterFactories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public VogenTypesFactory() { }
}
""";

context.AddSource("SystemTextJsonConverterFactory_g.cs", source);
context.AddSource("SystemTextJsonConverterFactory_g.cs", Util.FormatSource(source));
}

private static string BuildEntry(VoWorkItem eachStj)
Expand Down
6 changes: 6 additions & 0 deletions tests/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)..\'))" />
<PropertyGroup>
<NoWarn>$(NoWarn);NU1903</NoWarn>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -140,38 +140,29 @@ global::MongoDB.Bson.Serialization.BsonSerializer.TryRegisterSerializer(new What
// the code is regenerated.
// </auto-generated>
// ------------------------------------------------------------------------------

// Suppress warnings about [Obsolete] member usage in generated code.
#pragma warning disable CS0618

// Suppress warnings for 'Override methods on comparable types'.
#pragma warning disable CA1036

// Suppress Error MA0097 : A class that implements IComparable<T> or IComparable should override comparison operators
#pragma warning disable MA0097

// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.'
// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations.
#pragma warning disable CS8669, CS8632

// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
#pragma warning disable CS1591

namespace generator;

public class VogenTypesFactory : global::System.Text.Json.Serialization.JsonConverterFactory
{
public VogenTypesFactory() { }

private static readonly global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Lazy<global::System.Text.Json.Serialization.JsonConverter>> _lookup =
new global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Lazy<global::System.Text.Json.Serialization.JsonConverter>> {
};

public VogenTypesFactory()
{
}

private static readonly global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Lazy<global::System.Text.Json.Serialization.JsonConverter>> _lookup = new global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Lazy<global::System.Text.Json.Serialization.JsonConverter>>
{
};
public override bool CanConvert(global::System.Type typeToConvert) => _lookup.ContainsKey(typeToConvert);

public override global::System.Text.Json.Serialization.JsonConverter CreateConverter(global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) =>
_lookup[typeToConvert].Value;
public override global::System.Text.Json.Serialization.JsonConverter CreateConverter(global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) => _lookup[typeToConvert].Value;
}

// ------------------------------------------------------------------------------
Expand Down
Loading

0 comments on commit 57d1d84

Please sign in to comment.