From 90af87090f3247a6b2e6c199c729854542e32ca4 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Sun, 10 Dec 2023 08:30:06 +0000 Subject: [PATCH] More docs --- docs/site/Writerside/hi.tree | 3 + docs/site/Writerside/topics/Configuration.md | 25 + .../topics/Specifying-pre-set-values.md | 101 ++++ docs/site/Writerside/topics/Tutorials.topic | 5 +- .../site/Writerside/topics/Validate-values.md | 3 + .../Writerside/topics/discussions/Home.md | 550 +----------------- .../Writerside/topics/discussions/Overview.md | 70 ++- .../Writerside/topics/how-to/Installation.md | 11 +- docs/site/Writerside/topics/reference/FAQ.md | 428 +++++++++++++- .../topics/tutorials/Normalization.md | 4 +- .../topics/tutorials/NormalizationTutorial.md | 46 +- .../topics/tutorials/Serialization.md | 27 +- .../Writerside/topics/tutorials/Validation.md | 33 +- .../tutorials/your-first-value-object.md | 39 +- 14 files changed, 749 insertions(+), 596 deletions(-) create mode 100644 docs/site/Writerside/topics/Configuration.md create mode 100644 docs/site/Writerside/topics/Specifying-pre-set-values.md create mode 100644 docs/site/Writerside/topics/Validate-values.md diff --git a/docs/site/Writerside/hi.tree b/docs/site/Writerside/hi.tree index aca01567d3..06da2666e6 100644 --- a/docs/site/Writerside/hi.tree +++ b/docs/site/Writerside/hi.tree @@ -14,10 +14,12 @@ + + @@ -28,6 +30,7 @@ + diff --git a/docs/site/Writerside/topics/Configuration.md b/docs/site/Writerside/topics/Configuration.md new file mode 100644 index 0000000000..4c29cc27c5 --- /dev/null +++ b/docs/site/Writerside/topics/Configuration.md @@ -0,0 +1,25 @@ +# Configuration + +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 +* 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))] +``` + +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)` + +There are several code analysis warnings 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 +* when the combination of conversions does not match an entry diff --git a/docs/site/Writerside/topics/Specifying-pre-set-values.md b/docs/site/Writerside/topics/Specifying-pre-set-values.md new file mode 100644 index 0000000000..fcb7c77445 --- /dev/null +++ b/docs/site/Writerside/topics/Specifying-pre-set-values.md @@ -0,0 +1,101 @@ +# Specifying pre-set values + +In this tutorial, we'll look at how we can have pre-set values on our types. + +Pre-set values have two common uses: + +1. represent known values +2. represent values that cannot be externally created + +Let's look at the first scenario; representing known values. Create the following type: + +```c# +[ValueObject] +[Instance("WaterFreezingPoint", 0.0f)] +[Instance("WaterBoilingPoint", 100.0f)] +[Instance("AbsoluteZero", -273.15f)] +public partial struct Centigrade { } +``` + +You can now use it like so: + +```C# +Console.WriteLine(Centigrade.WaterFreezingPoint); +Console.WriteLine(Centigrade.WaterBoilingPoint); +Console.WriteLine(Centigrade.AbsoluteZero); +``` + +... resulting in + +``` +0 +100 +-273.15 +``` + +These known instances can bring domain terms into your code; for instance, it's easier to read this than +numeric literals of `0` and `273.15`: + +```C# +if(waterTemperature == Centigrade.WaterFreezingPoint) ... +``` + +Now, let's take a look at the other scenario of representing values that can't (and **shouldn't**) be +created externally. The term 'externally' user here, means **users** of the class. + +Let's revisit our `CustomerId` from the [validation tutorial](Validation.md). We want to say that an instance +with a value of zero means that the customer was not specified, but we don't want users to explicitly create +instances with a value of zero. Let's try it out. Create this type again: + +```C# +[ValueObject] +public partial struct CustomerId +{ + private static Validation Validate(int input) => input > 0 + ? Validation.Ok + : Validation.Invalid("Customer IDs must be greater than 0."); +} +``` + +We know, from the validation tutorial that this throws an exception. This means that users can't create one with +a zero value. All well and good. But **we** (the author of the type), want to create one with a zero. + +We can do this with a known-instance: + +```C# +[ValueObject] +[Instance("Unspecified", 0)] +public partial struct CustomerId +{ + private static Validation Validate(int input) => input > 0 + ? Validation.Ok + : Validation.Invalid("Customer IDs must be greater than 0."); +} +``` + +We can now use the instance of an unspecified customer id: + +```C# +Console.WriteLine(CustomerId.Unspecified); + +>> 0 +``` + +This can be useful in representing optional or missing data in your domain, e.g. + +```C# +public CustomerId TryGetOptionalCustomerId(string input) +{ + if (string.IsNullOrEmpty(input)) + { + return CustomerId.Unspecified; + } + + return CustomerId.From(123); +} +``` + +This again makes the domain easier to read and eliminates a scenario where a `null` might otherwise be used. + +There are other ways to declare instances, and there is special consideration for dates. These are covered in +this [How-to article](Instances.md) diff --git a/docs/site/Writerside/topics/Tutorials.topic b/docs/site/Writerside/topics/Tutorials.topic index eebbdf5ebe..20f5aa67a3 100644 --- a/docs/site/Writerside/topics/Tutorials.topic +++ b/docs/site/Writerside/topics/Tutorials.topic @@ -18,8 +18,9 @@ Tutorials - These focus on how to achieve various things using Vogen, from creating your first - value object, to handling nuances of decimals when deserializing from JavaScript. + These tutorials focus on how to achieve various things using Vogen, from creating your first + value object, to more advances scenarios like handling the nuances of decimals when + deserializing from JavaScript. diff --git a/docs/site/Writerside/topics/Validate-values.md b/docs/site/Writerside/topics/Validate-values.md new file mode 100644 index 0000000000..448fa5de3b --- /dev/null +++ b/docs/site/Writerside/topics/Validate-values.md @@ -0,0 +1,3 @@ +# Validate values + +Start typing here... \ No newline at end of file diff --git a/docs/site/Writerside/topics/discussions/Home.md b/docs/site/Writerside/topics/discussions/Home.md index 3347ed28bd..c12fccc6fb 100644 --- a/docs/site/Writerside/topics/discussions/Home.md +++ b/docs/site/Writerside/topics/discussions/Home.md @@ -26,9 +26,10 @@ So, we need some validation to ensure the **constraints** of a customer ID are m sure if it's been checked beforehand, so we need to check it every time we use it. Because it's a primitive, someone might've changed the value, so even if we're 100% sure we've checked it before, it still might need checking again. -So far, we've used as an example, a customer ID of value `42`. In C#, it may come as no surprise that "`42 == 42`" -(*I haven't checked that in JavaScript!*). But in our **domain**, should `42` always equal `42`? Probably not if -you're comparing a Supplier ID of `42` to a Customer ID of `42`! But primitives won't help you here (remember, `42 == 42`!). +So far, we've used as an example, a customer ID of value 42. In C#, it may come as no surprise that this +is true: "`42 == 42`" (*I haven't checked that in JavaScript!*). But in our **domain**, should 42 always +equal 42? Probably not if you're comparing a **Supplier ID** of 42 to a **Customer ID** of 42! But primitives +won't help you here (remember, `42 == 42`!): ```c# (42 == 42) // true @@ -44,7 +45,9 @@ public class Person { } ``` -We can do that with an `Instance` attribute: +We can do that with _Instances_, which is covered more in [this tutorial](Specifying-pre-set-values.md). +The code below specifies an instance named `Unspecified` which has a value of `-1`, but disallows anyone else to +create one with such a number. More information on validation can be found in [this tutorial](Validation.md). ```c# [ValueObject] @@ -57,7 +60,8 @@ We can do that with an `Instance` attribute: } ``` -This generates `public static Age Unspecified = new Age(-1);`. The constructor is `private`, so only this type can (deliberately) create _invalid_ instances. +This generates `public static Age Unspecified = new Age(-1);`. +The constructor is `private`, so only this type can (deliberately) create _invalid_ instances. Now, when we use `Age`, our validation becomes clearer: @@ -79,538 +83,4 @@ public readonly partial struct Celsius { public static Validation Validate(float value) => value >= -273 ? Validation.Ok : Validation.Invalid("Cannot be colder than absolute zero"); } -``` - -## Configuration - -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 -* 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))] -``` - -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)` - -There are several code analysis warnings 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 -* when the combination of conversions does not match an entry - -## Performance - -(to run these yourself: `dotnet run -c Release --framework net7.0 -- --job short --filter *` in the `Vogen.Benchmarks` folder) - -As mentioned previously, the goal of Vogen is to achieve very similar performance compared to using primitives themselves. -Here's a benchmark comparing the use of a validated Value Object with an underlying type of `int` vs using an `int` natively (*primitively* 🤓) - -``` ini -BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22621.1194) -AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores -.NET SDK=7.0.102 - [Host] : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2 - ShortRun : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2 -Job=ShortRun IterationCount=3 LaunchCount=1 -WarmupCount=3 -``` - -| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | -|:----------------------:|:--------:|:--------:|:--------:|:-----:|:-------:|:----:|:---------:| -| UsingIntNatively | 14.55 ns | 1.443 ns | 0.079 ns | 1.00 | 0.00 | - | - | -| UsingValueObjectStruct | 14.88 ns | 3.639 ns | 0.199 ns | 1.02 | 0.02 | - | - | - -There is no discernible difference between using a native int and a VO struct; both are pretty much the same in terms of speed and memory. - -The next most common scenario is using a VO class to represent a native `String`. These results are: - -``` ini -BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22621.1194) -AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores -.NET SDK=7.0.102 - [Host] : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2 - ShortRun : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2 -Job=ShortRun IterationCount=3 LaunchCount=1 -WarmupCount=3 -``` - -| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | -|--------------------------|----------|-------|--------|-------|---------|--------|-----------|-------------| -| UsingStringNatively | 151.8 ns | 32.19 | 1.76 | 1.00 | 0.00 | 0.0153 | 256 B | 1.00 | -| UsingValueObjectAsStruct | 184.8 ns | 12.19 | 0.67 | 1.22 | 0.02 | 0.0153 | 256 B | 1.00 | - - -There is a tiny amount of performance overhead, but these measurements are incredibly small. There is no memory overhead. - -## Serialisation and type conversion - -By default, each VO is decorated with a `TypeConverter` and `System.Text.Json` (STJ) serializer. There are other converters/serializer for: - -* Newtonsoft.Json (NSJ) -* Dapper -* EFCore -* [LINQ to DB](https://github.com/linq2db/linq2db) -* protobuf-net (see the FAQ section below) - -They are controlled by the `Conversions` enum. The following has serializers for NSJ and STJ: - -```c# -[ValueObject(conversions: Conversions.NewtonsoftJson | Conversions.SystemTextJson, underlyingType: typeof(float))] -public readonly partial struct Celsius { } -``` - -If you don't want any conversions, then specify `Conversions.None`. - -If you want your own conversion, then again specify none, and implement them yourself, just like any other type. But be aware that even serializers will get the same compilation errors for `new` and `default` when trying to create VOs. - -If you want to use Dapper, remember to register it—something like this: - -```c# -SqlMapper.AddTypeHandler(new Customer.DapperTypeHandler()); -``` - -See the examples folder for more information. - -## FAQ - -### Is there a Wiki for this project? - -Yes, it's here: https://github.com/SteveDunn/Vogen/wiki - -### What versions of .NET are supported? - -The source generator is .NET Standard 2.0. The code it generates supports all C# language versions from 6.0 and onwards - -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: - -```xml - - - -``` - -* set the language version to `latest` (or anything `8` or more): - -```c# - -+ latest - Debug -``` - -### 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. - -Source generation is driven by attributes, and, if you're using .NET 7 or above, the generic version of the `ValueObject` attribute is exposed: - -```c# -[ValueObject] -public partial struct Age { } -``` - -### Why are they called 'Value Objects'? - -The term Value Object represents a small object whose equality is based on value and not identity. From [Wikipedia](https://en.wikipedia.org/wiki/Value_object) - -> _In computer science, a Value Object is a small object that represents a simple entity whose equality is not based on identity: i.e., two Value Objects are equal when they have the same value, not necessarily being the same object._ - -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_ - -### How can I view the code that is generated? - -Add this to your `.csproj` file: - -```xml - - true - Generated - - - - - -``` - -Then, you can view the generated files in the `Generated` folder. In Visual Studio, you need to select 'Show all files' in the Solution Explorer window: - -![the solution explorer window shows the 'show all files' option](20220425061514.png) - -Here's an example from the included `Samples` project: - -![the solution explorer window showing generated files](20220425061733.png) - -### Why can't I just use `public record struct CustomerId(int Value);`? - -That doesn't give you validation. To validate `Value`, you can't use the shorthand syntax (Primary Constructor). So you'd need to do: - -```c# -public record struct CustomerId -{ - public CustomerId(int value) { - if(value <=0) throw new Exception(...) - } -} -``` - -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. 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.: - -```c# -// error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. -CustomerId c = default; - -// error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. -var c2 = default(CustomerId); -``` - -### Can I serialize and deserialize them? - -Yes. By default, each VO is decorated with a `TypeConverter` and `System.Text.Json` (STJ) serializer. There are other converters/serializers for: - -* Newtonsoft.Json (NSJ) -* Dapper -* EFCore -* LINQ to DB - -### Can I use them in EFCore? - -Yes, although there are certain considerations. [Please see the EFCore page on the Wiki](https://github.com/SteveDunn/Vogen/wiki/Value-Objects-in-EFCore), -but the TL;DR is: - -* If the Value Object on your entity is a struct, then you don't need to do anything special - -* But if it is a class, then you need a conversion to be generated, e.g. `[ValueObject(conversions: Conversions.EfCoreValueConverter)]` - and you need to tell EFCore to use that converter in the `OnModelCreating` method, e.g.: - -```c# - builder.Entity(b => - { - b.Property(e => e.Name).HasConversion(new Name.EfCoreValueConverter()); - }); -``` - - -### It seems like a lot of overhead; I can validate the value myself when I use it! - -You could, but to ensure consistency throughout your domain, you'd have to **validate everywhere**. And Shallow's Law says that that's not possible: - -> ⚖️ **Shalloway's Law** -> *"when N things need to change and N > 1, Shalloway will find at most N - 1 of these things."* - -Concretely: *"When 5 things need to change, Shalloway will find at most, 4 of these things."* - -### If my VO is a `struct`, can I prohibit the use of `CustomerId customerId = default(CustomerId);`? - -**Yes**. The analyzer generates a compilation error. - -### If my VO is a `struct`, can I prohibit the use of `CustomerId customerId = new(CustomerId);`? - -**Yes**. The analyzer generates a compilation error. - -### If my VO is a struct, can I have my own constructor? - -**No**. The parameter-less constructor is generated automatically, and the constructor that takes the underlying value is also generated automatically. - -If you add further constructors, then you will get a compilation error from the code generator, e.g. - -```c# -[ValueObject(typeof(int))] -public partial struct CustomerId { - // Vogen already generates this as a private constructor: - // error CS0111: Type 'CustomerId' already defines a member called 'CustomerId' with the same parameter type - public CustomerId() { } - - // error VOG008: Cannot have user defined constructors, please use the From method for creation. - public CustomerId(int value) { } -} -``` - -### If my VO is a struct, can I have my own fields? - -You *could*, but you'd get compiler warning [CS0282-There is no defined ordering between fields in multiple declarations of partial class or struct 'type'](https://docs.microsoft.com/en-us/dotnet/csharp/misc/cs0282) - -### Why are there, by default, no implicit conversions to and from the primitive types that are being wrapped? - -Implicit operators can be useful, but for Value Objects, they can confuse things. Take the following code **without** any implicit conversions: - -```c# -Age age1 = Age.From(1); -OsVersion osVersion = OsVersion.From(1); - -Console.WriteLine(age1 == osVersion); // won't compile! \o/ -``` - -That makes perfect sense. But adding in an implicit operator **from** `Age` **to** `int`, and it does compile! - -`Console.WriteLine(age1 == osVersion); // TRUE! (◎_◎;)` - -If we remove that implicit operator and replace it with an implicit operator **from** `int` **to** `Age`, it no longer compiles, which is great (we've got type safety back), but we end up [violating the rules of implicit operators](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/user-defined-conversion-operators): - -> Predefined C# implicit conversions always succeed and never throw an exception. User-defined implicit conversions should behave in that way as well. If a custom conversion can throw an exception or lose information, define it as an explicit conversion - -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. - -Which is interesting—Vogen _wouldn't_ throw an `InvalidCastException` (only an `ValueObjectValidationException`). Also, for `struct`s, we _wouldn't_ create a heap allocation. - -But since users of Vogen can declare a Value Object as a `class` **or** `struct`, then we wouldn't want implicit operators (from `primitive` => `ValueObject`) for just `structs` and not `class`es. - -### Can you opt in to implicit conversions? - -Yes, by specifying the `toPrimitiveCasting` and `fromPrimitiveCasting` in either local or global config. -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`): - -```c# -[ValueObject(typeof(int))] -public readonly partial struct Age { - public static Validation Validate(int n) => n >= 0 ? Validation.Ok : Validation.Invalid("Must be zero or more"); -} -``` - -That says that `Age` instances can never be negative. So you would probably expect the following to throw, but it doesn't: - -```c# -var age20 = Age.From(20); -var age10 = age20 / 2; -++age10; -age10 -= 12; // bang - goes negative?? -``` - -The implicit cast in `var age10 = age20 / 2` results in an `int` and not an `Age`. Changing it to `Age age10 = age20 / 2` fixes it. But this does go to show that it can be confusing. - -### Why is there no interface? - -> _If I'm using a library that uses Vogen, I'd like to easily tell if the type is just a primitive wrapper or not by the fact that it implements an interface, such as `IValidated`_ - -Just like primitives have no interfaces, there's no need to have interfaces on Value Objects. The receiver that takes a `CustomerId` knows that it's a Value Object. If it were instead to take an `IValidated`, then it wouldn't have any more information; you'd still have to know to call `Value` to get the value. - -It might also relax type-safety. Without the interface, we have signatures such as this: - -```c# -public void SomSomething(CustomerId customerId, SupplierId supplierId, ProductId productId); -``` - -... but with the interface, we _could_ have signatures such as this: - -```c# -public void SomSomething(IValidate customerId, IValidated supplierId, IValidated productId); -``` - -So, callers could mess things up by calling `DoSomething(productId, supplierId, customerId)`) - -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. - -### Can I represent special values - -Yes. You might want to represent special values for things like invalid or unspecified instances, e.g. - -```c# -/* -* Instances are the only way to avoid validation, so we can create instances -* that nobody else can. This is useful for creating special instances -* that represent concepts such as 'invalid' and 'unspecified'. -*/ -[ValueObject] -[Instance("Unspecified", -1)] -[Instance("Invalid", -2)] -public readonly partial struct Age -{ - private static Validation Validate(int value) => - value > 0 ? Validation.Ok : Validation.Invalid("Must be greater than zero."); -} -``` - -You can then use default values when using these types, e.g. - -```c# -public class Person { - public Age Age { get; set; } = Age.Unspecified -} -``` - -... and if you take an Age, you can compare it to an instance that is invalid/unspecified - -```c# -public void CanEnter(Age age) { - if(age == Age.Unspecified || age == Age.Invalid) throw CannotEnterException("Name not specified or is invalid") - return age < 17; -} -``` - -### Can I normalize the value when a VO is created? -I'd like to normalize/sanitize the values used, for example, trimming the input. Is this possible? - -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. - - -### 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 -{ - // This attribute will default to having both the default conversions and EF Core type conversions - public CustomValueObjectAttribute(Conversions conversions = Conversions.Default | Conversions.EfCoreValueConverter) { } -} -``` - -NOTE: *custom attributes must extend a ValueObjectAttribute class; you cannot 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. - -### How do I run the benchmarks? - -`dotnet run -c Release -- --job short --framework net6.0 --filter *` - -### Why do I get a build error when running `.\Build.ps1`? - -You might see this: -``` -.\Build.ps1 : File C:\Build.ps1 cannot be loaded. The file C:\Build.ps1 is not digitally signed. You cannot run this script on the current system. -``` - -To get around this, run `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass` - -### What alternatives are there? - -[StronglyTypedId](https://github.com/andrewlock/StronglyTypedId) -This is focused more on IDs. Vogen is focused more on 'Domain Concepts' and the constraints associated with those concepts. - -[StringlyTyped](https://github.com/stevedunn/stringlytyped) -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. - -[ValueObjectGenerator](https://github.com/RyotaMurohoshi/ValueObjectGenerator) -Similar to Vogen, but less focused on validation and no code analyzer. - -### What primitive types are supported? - -Any type can be wrapped. Serialisation and type conversions have implementations for: -* string - -* int -* long -* short -* byte - -* float (Single) -* decimal -* double - -* DateTime -* DateOnly -* TimeOnly -* DateTimeOffset - -* Guid - -* bool - -For other types, generic type conversion and a serializer are applied. If you are supplying your own converters for type -conversion and serialization, then specify `None` for converters and decorate your type with attributes for your own types, e.g. - -```c# -[ValueObject(typeof(SpecialPrimitive), conversions: Conversions.None)] -[System.Text.Json.Serialization.JsonConverter(typeof(SpecialPrimitiveJsonConverter))] -[System.ComponentModel.TypeConverter(typeof(SpecialPrimitiveTypeConverter))] -public partial struct SpecialMeasurement { } -``` - -### I've made a change that means the 'Snapshot' tests are expectedly failing in the build—what do I do? - -Vogen uses a combination of unit tests, in-memory compilation tests, and snapshot tests. The snapshot tests are used -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 -tool, or, if there are a lot 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). - -**NOTE: If the change to the source generators expectedly changes the majority of the snapshot tests, then you can tell the -snapshot runner to overwrite the expected files with the actual files that are generated.** - -To do this, run `.\Build.ps1 -v "Minimal" -resetSnapshots $true`. This deletes all `snaphsot` folders under the `tests` folder -and treats everything generated as the new baseline for future comparisons. - -This will mean that there are potentially **thousands** of changed files that will end up in the commit, 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. - -To debug an analyzer, select or write a test in the AnalyzerTests. There are tests that exercise the various analyzers and code-fixers. - -### How do I run the tests that actually use the source generator? - -It is challenging to run tests that _use_ the source generator in the same project **as** the source generator, so there -is a separate solution for this. It's called `Consumers.sln`. What happens is that `build.ps1` builds the generator, runs -the tests, and creates the NuGet package _in a private local folder_. The package is version `999.9.xxx` and the consumer -references the latest version. The consumer can then really use the source generator, just like anything else. - -> Note: if you don't want to run the lengthy snapshot tests when building the local nuget package, run `.\Build.ps1 -v "minimal" -skiptests $true` - -### Can I get it to throw my own exception? - -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? - -Linq2DB 4.0 or greater supports `DateOnly` and `TimeOnly`. Vogen generates value converters for Linq2DB; for `DateOnly`, it just works, but for `TimeOnly, you need to add this to your application: - -`MappingSchema.Default.SetConverter(dt => TimeOnly.FromDateTime(dt));` - -### Can I use protobuf-net? - -Yes. Add a dependency to protobuf-net and set a surrogate attribute: - -```c# -[ValueObject(typeof(string))] -[ProtoContract(Surrogate = typeof(string))] -public partial class BoxId { -//... -} -``` - -The BoxId type will now be serialized as a `string` in all messages and grpc calls. If one is generating `.proto` files -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_. - - -## Attribution - -I took inspiration from [Andrew Lock's](https://github.com/andrewlock) [StronglyTypedId](https://github.com/andrewlock/StronglyTypedId). - -I got some great ideas from [Gérald Barré's](https://github.com/meziantou) [Meziantou.Analyzer](https://github.com/meziantou/Meziantou.Analyzer) \ No newline at end of file +``` \ No newline at end of file diff --git a/docs/site/Writerside/topics/discussions/Overview.md b/docs/site/Writerside/topics/discussions/Overview.md index c431adabb6..25c87f98ce 100644 --- a/docs/site/Writerside/topics/discussions/Overview.md +++ b/docs/site/Writerside/topics/discussions/Overview.md @@ -62,22 +62,21 @@ public void SendInvoice(CustomerId customerId) { ... } The main goal of Vogen is to **ensure the validity of your Value Objects**. It does this with code analyzers that add constraints to C#. - -The analyzer spots situations where you might end up with uninitialized Value Objects in your domain. +The analyzers spot situations where you might end up with uninitialized Value Objects in your domain. These analyzers, by default, cause compilation errors. -There are a few ways you could end up with uninitialized Value Objects. +There are a few ways you can end up with uninitialized Value Objects. One way is by giving your type constructors. Providing your own constructors could mean that you -forget to set a value, so **Vogen doesn't allow you to have user defined constructors**: +forget to set a value, so **Vogen doesn't allow user defined constructors**: ```c# [ValueObject] public partial struct CustomerId { - // Vogen deliberately generates this - // so that you can't create your own. // error CS0111: Type 'CustomerId' already defines a member called // 'CustomerId' with the same parameter type + // That's because Vogen deliberately generates this + // so that you can't create your own. public CustomerId() { } // error VOG008: Cannot have user defined constructors, please use @@ -89,28 +88,50 @@ public partial struct CustomerId In addition, Vogen will spot issues when **creating** or **consuming** Value Objects: ```c# -// catches object creation expressions -var c = new CustomerId(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited -CustomerId c = default; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. +// --- catches object creation expressions + +// error VOG010: Type 'CustomerId' cannot be constructed +// with 'new' as it is prohibited +var c = new CustomerId(); + +// error VOG009: ... cannot be constructed with default ... +CustomerId c = default; + +// error VOG009: ... cannot be constructed with default ... +var c = default(CustomerId); + -var c = default(CustomerId); // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. -var c = GetCustomerId(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited +// error VOG010: ... cannot be constructed with 'new' ... +var c = GetCustomerId(); -var c = Activator.CreateInstance(); // error VOG025: Type 'CustomerId' cannot be constructed via Reflection as it is prohibited. -var c = Activator.CreateInstance(typeof(CustomerId)); // error VOG025: Type 'MyVo' cannot be constructed via Reflection as it is prohibited +// error VOG025: Type 'CustomerId' cannot be constructed via Reflection +var c = Activator.CreateInstance(); -// catches lambda expressions -Func f = () => default; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. +// error VOG025: Type 'MyVo' cannot be constructed via Reflection +var c = Activator.CreateInstance(typeof(CustomerId)); -// catches method / local function return expressions -CustomerId GetCustomerId() => default; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. -CustomerId GetCustomerId() => new CustomerId(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited -CustomerId GetCustomerId() => new(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited +// catches lambda expressions, e.g. +// error VOG009: Type 'CustomerId' cannot be constructed with default +Func f = () => default; -// catches argument / parameter expressions -Task t = Task.FromResult(new()); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited +// --- catches method / local function return expressions, e.g. -void Process(CustomerId customerId = default) { } // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. +// error VOG009: Type 'CustomerId' cannot be constructed with default +CustomerId GetCustomerId() => default; + +// error VOG010: Type 'CustomerId' cannot be constructed with 'new' +CustomerId GetCustomerId() => new CustomerId(); + +// error VOG010: Type 'CustomerId' cannot be constructed with 'new' +CustomerId GetCustomerId() => new(); + +// --- catches argument / parameter expressions + +// error VOG010: Type 'CustomerId' cannot be constructed with 'new' +Task t = Task.FromResult(new()); + +// error VOG009: Type 'CustomerId' cannot be constructed with default +void Process(CustomerId customerId = default) { } ``` One of the main goals of Vogen is to achieve **almost the same speed and memory performance as using @@ -118,3 +139,8 @@ primitives directly**. Put another way, if your `decimal` primitive represents an Account Balance, then there is **extremely** low overhead of using an `AccountBalance` Value Object instead. Please see the [performance metrics](Performance.md) for more information. + +## Attribution + +I took inspiration from [Andrew Lock's](https://github.com/andrewlock) [StronglyTypedId](https://github.com/andrewlock/StronglyTypedId) and got some great ideas +from [Gérald Barré's](https://github.com/meziantou) [Meziantou.Analyzer](https://github.com/meziantou/Meziantou.Analyzer) diff --git a/docs/site/Writerside/topics/how-to/Installation.md b/docs/site/Writerside/topics/how-to/Installation.md index 06979077e8..e9e25fb8a0 100644 --- a/docs/site/Writerside/topics/how-to/Installation.md +++ b/docs/site/Writerside/topics/how-to/Installation.md @@ -1,10 +1,10 @@ # Installing Vogen -Change `3.0.23` for the latest version listed on NuGet +These tutorials assume a working knowledge of .NET and C#, so won't include the basics necessary to start the +tutorials, e.g. things like creating new projects, creating new types, compiling, viewing error output, etc. - dotnet add package Vogen --version 3.0.23 @@ -21,5 +21,10 @@ Change `3.0.23` for the latest ve + +Change `3.0.23` for the latest version listed on NuGet + + + When added to your project, the **source generator** generates the wrappers for your primitives and the **code analyzer** -will let you know if you try to create invalid Value Objects. +will let you know if you try to create invalid Value Objects. diff --git a/docs/site/Writerside/topics/reference/FAQ.md b/docs/site/Writerside/topics/reference/FAQ.md index 4b839164f1..37a75f0e33 100644 --- a/docs/site/Writerside/topics/reference/FAQ.md +++ b/docs/site/Writerside/topics/reference/FAQ.md @@ -38,4 +38,430 @@ 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) \ No newline at end of file +**A: ** You can use this information on [this page](How-to-identify-a-type-that-is-generated-by-Vogen.md) + + +### What versions of .NET are supported? + +The source generator is .NET Standard 2.0. The code it generates supports all C# language versions from 6.0 and onwards + +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: + +```xml + + + +``` + +* set the language version to `latest` (or anything `8` or more): + +```c# + ++ latest + Debug +``` + +### 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. + +Source generation is driven by attributes, and, if you're using .NET 7 or above, the generic version of the `ValueObject` attribute is exposed: + +```c# +[ValueObject] +public partial struct Age { } +``` + +### Why are they called 'Value Objects'? + +The term Value Object represents a small object whose equality is based on value and not identity. From [Wikipedia](https://en.wikipedia.org/wiki/Value_object) + +> _In computer science, a Value Object is a small object that represents a simple entity whose equality is not based on identity: i.e., two Value Objects are equal when they have the same value, not necessarily being the same object._ + +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_ + +### How can I view the code that is generated? + +Add this to your `.csproj` file: + +```xml + + true + Generated + + + + + +``` + +Then, you can view the generated files in the `Generated` folder. In Visual Studio, you need to select 'Show all files' in the Solution Explorer window: + +![the solution explorer window shows the 'show all files' option](20220425061514.png) + +Here's an example from the included `Samples` project: + +![the solution explorer window showing generated files](20220425061733.png) + +### Why can't I just use `public record struct CustomerId(int Value);`? + +That doesn't give you validation. To validate `Value`, you can't use the shorthand syntax (Primary Constructor). +So you'd need to do: + +```c# +public record struct CustomerId +{ + public CustomerId(int value) { + if(value <=0) throw new Exception(...) + } +} +``` + +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. +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.: + +```c# +// error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. +CustomerId c = default; + +// error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. +var c2 = default(CustomerId); +``` + +### Can I serialize and deserialize them? + +Yes. By default, each VO is decorated with a `TypeConverter` and `System.Text.Json` (STJ) serializer. There are other converters/serializers for: + +* Newtonsoft.Json (NSJ) +* Dapper +* EFCore +* LINQ to DB + +### Can I use them in EFCore? + +Yes, although there are certain considerations. [Please see the EFCore page on the Wiki](https://github.com/SteveDunn/Vogen/wiki/Value-Objects-in-EFCore), +but the TL;DR is: + +* If the Value Object on your entity is a struct, then you don't need to do anything special + +* But if it is a class, then you need a conversion to be generated, e.g. `[ValueObject(conversions: Conversions.EfCoreValueConverter)]` + and you need to tell EFCore to use that converter in the `OnModelCreating` method, e.g.: + +```c# + builder.Entity(b => + { + b.Property(e => e.Name).HasConversion(new Name.EfCoreValueConverter()); + }); +``` + + +### It seems like a lot of overhead; I can validate the value myself when I use it! + +You could, but to ensure consistency throughout your domain, you'd have to **validate everywhere**. And Shallow's Law says that that's not possible: + +> ⚖️ **Shalloway's Law** +> *"when N things need to change and N > 1, Shalloway will find at most N - 1 of these things."* + +Concretely: *"When 5 things need to change, Shalloway will find at most, 4 of these things."* + +### If my VO is a `struct`, can I prohibit the use of `CustomerId customerId = default(CustomerId);`? + +**Yes**. The analyzer generates a compilation error. + +### If my VO is a `struct`, can I prohibit the use of `CustomerId customerId = new(CustomerId);`? + +**Yes**. The analyzer generates a compilation error. + +### If my VO is a struct, can I have my own constructor? + +**No**. The parameter-less constructor is generated automatically, and the constructor that takes the underlying value is also generated automatically. + +If you add further constructors, then you will get a compilation error from the code generator, e.g. + +```c# +[ValueObject(typeof(int))] +public partial struct CustomerId { + // Vogen already generates this as a private constructor: + // error CS0111: Type 'CustomerId' already defines a member called 'CustomerId' with the same parameter type + public CustomerId() { } + + // error VOG008: Cannot have user defined constructors, please use the From method for creation. + public CustomerId(int value) { } +} +``` + +### If my VO is a struct, can I have my own fields? + +You *could*, but you'd get compiler warning [CS0282-There is no defined ordering between fields in multiple declarations of partial class or struct 'type'](https://docs.microsoft.com/en-us/dotnet/csharp/misc/cs0282) + +### Why are there, by default, no implicit conversions to and from the primitive types that are being wrapped? + +Implicit operators can be useful, but for Value Objects, they can confuse things. Take the following code **without** any implicit conversions: + +```c# +Age age1 = Age.From(1); +OsVersion osVersion = OsVersion.From(1); + +Console.WriteLine(age1 == osVersion); // won't compile! \o/ +``` + +That makes perfect sense. But adding in an implicit operator **from** `Age` **to** `int`, and it does compile! + +`Console.WriteLine(age1 == osVersion); // TRUE! (◎_◎;)` + +If we remove that implicit operator and replace it with an implicit operator **from** `int` **to** `Age`, it no longer compiles, which is great (we've got type safety back), but we end up [violating the rules of implicit operators](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/user-defined-conversion-operators): + +> Predefined C# implicit conversions always succeed and never throw an exception. User-defined implicit conversions should behave in that way as well. If a custom conversion can throw an exception or lose information, define it as an explicit conversion + +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. + +Which is interesting—Vogen _wouldn't_ throw an `InvalidCastException` (only an `ValueObjectValidationException`). Also, for `struct`s, we _wouldn't_ create a heap allocation. + +But since users of Vogen can declare a Value Object as a `class` **or** `struct`, then we wouldn't want implicit operators (from `primitive` => `ValueObject`) for just `structs` and not `class`es. + +### Can you opt in to implicit conversions? + +Yes, by specifying the `toPrimitiveCasting` and `fromPrimitiveCasting` in either local or global config. +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`): + +```c# +[ValueObject(typeof(int))] +public readonly partial struct Age { + public static Validation Validate(int n) => n >= 0 ? Validation.Ok : Validation.Invalid("Must be zero or more"); +} +``` + +That says that `Age` instances can never be negative. So you would probably expect the following to throw, but it doesn't: + +```c# +var age20 = Age.From(20); +var age10 = age20 / 2; +++age10; +age10 -= 12; // bang - goes negative?? +``` + +The implicit cast in `var age10 = age20 / 2` results in an `int` and not an `Age`. Changing it to `Age age10 = age20 / 2` fixes it. But this does go to show that it can be confusing. + +### Why is there no interface? + +> _If I'm using a library that uses Vogen, I'd like to easily tell if the type is just a primitive wrapper or not by the fact that it implements an interface, such as `IValidated`_ + +Just like primitives have no interfaces, there's no need to have interfaces on Value Objects. The receiver that takes a `CustomerId` knows that it's a Value Object. If it were instead to take an `IValidated`, then it wouldn't have any more information; you'd still have to know to call `Value` to get the value. + +It might also relax type-safety. Without the interface, we have signatures such as this: + +```c# +public void SomSomething(CustomerId customerId, SupplierId supplierId, ProductId productId); +``` + +... but with the interface, we _could_ have signatures such as this: + +```c# +public void SomSomething(IValidate customerId, IValidated supplierId, IValidated productId); +``` + +So, callers could mess things up by calling `DoSomething(productId, supplierId, customerId)`) + +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. + +### Can I represent special values + +Yes. You might want to represent special values for things like invalid or unspecified instances, e.g. + +```c# +/* +* Instances are the only way to avoid validation, so we can create instances +* that nobody else can. This is useful for creating special instances +* that represent concepts such as 'invalid' and 'unspecified'. +*/ +[ValueObject] +[Instance("Unspecified", -1)] +[Instance("Invalid", -2)] +public readonly partial struct Age +{ + private static Validation Validate(int value) => + value > 0 ? Validation.Ok : Validation.Invalid("Must be greater than zero."); +} +``` + +You can then use default values when using these types, e.g. + +```c# +public class Person { + public Age Age { get; set; } = Age.Unspecified +} +``` + +... and if you take an Age, you can compare it to an instance that is invalid/unspecified + +```c# +public void CanEnter(Age age) { + if(age == Age.Unspecified || age == Age.Invalid) throw CannotEnterException("Name not specified or is invalid") + return age < 17; +} +``` + +### Can I normalize the value when a VO is created? +I'd like to normalize/sanitize the values used, for example, trimming the input. Is this possible? + +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. + + +### 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 +{ + // This attribute will default to having both the default conversions and EF Core type conversions + public CustomValueObjectAttribute(Conversions conversions = Conversions.Default | Conversions.EfCoreValueConverter) { } +} +``` + +NOTE: *custom attributes must extend a ValueObjectAttribute class; you cannot 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. + +### How do I run the benchmarks? + +`dotnet run -c Release -- --job short --framework net6.0 --filter *` + +### Why do I get a build error when running `.\Build.ps1`? + +You might see this: +``` +.\Build.ps1 : File C:\Build.ps1 cannot be loaded. The file C:\Build.ps1 is not digitally signed. You cannot run this script on the current system. +``` + +To get around this, run `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass` + +### What alternatives are there? + +[StronglyTypedId](https://github.com/andrewlock/StronglyTypedId) +This is focused more on IDs. Vogen is focused more on 'Domain Concepts' and the constraints associated with those concepts. + +[StringlyTyped](https://github.com/stevedunn/stringlytyped) +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. + +[ValueObjectGenerator](https://github.com/RyotaMurohoshi/ValueObjectGenerator) +Similar to Vogen, but less focused on validation and no code analyzer. + +### What primitive types are supported? + +Any type can be wrapped. Serialisation and type conversions have implementations for: +* string + +* int +* long +* short +* byte + +* float (Single) +* decimal +* double + +* DateTime +* DateOnly +* TimeOnly +* DateTimeOffset + +* Guid + +* bool + +For other types, generic type conversion and a serializer are applied. If you are supplying your own converters for type +conversion and serialization, then specify `None` for converters and decorate your type with attributes for your own types, e.g. + +```c# +[ValueObject(typeof(SpecialPrimitive), conversions: Conversions.None)] +[System.Text.Json.Serialization.JsonConverter(typeof(SpecialPrimitiveJsonConverter))] +[System.ComponentModel.TypeConverter(typeof(SpecialPrimitiveTypeConverter))] +public partial struct SpecialMeasurement { } +``` + +### I've made a change that means the 'Snapshot' tests are expectedly failing in the build—what do I do? + +Vogen uses a combination of unit tests, in-memory compilation tests, and snapshot tests. The snapshot tests are used +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 +tool, or, if there are a lot 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). + +**NOTE: If the change to the source generators expectedly changes the majority of the snapshot tests, then you can tell the +snapshot runner to overwrite the expected files with the actual files that are generated.** + +To do this, run `.\Build.ps1 -v "Minimal" -resetSnapshots $true`. This deletes all `snaphsot` folders under the `tests` folder +and treats everything generated as the new baseline for future comparisons. + +This will mean that there are potentially **thousands** of changed files that will end up in the commit, 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. + +To debug an analyzer, select or write a test in the AnalyzerTests. There are tests that exercise the various analyzers and code-fixers. + +### How do I run the tests that actually use the source generator? + +It is challenging to run tests that _use_ the source generator in the same project **as** the source generator, so there +is a separate solution for this. It's called `Consumers.sln`. What happens is that `build.ps1` builds the generator, runs +the tests, and creates the NuGet package _in a private local folder_. The package is version `999.9.xxx` and the consumer +references the latest version. The consumer can then really use the source generator, just like anything else. + +> Note: if you don't want to run the lengthy snapshot tests when building the local nuget package, run `.\Build.ps1 -v "minimal" -skiptests $true` + +### Can I get it to throw my own exception? + +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? + +Linq2DB 4.0 or greater supports `DateOnly` and `TimeOnly`. Vogen generates value converters for Linq2DB; for `DateOnly`, it just works, but for `TimeOnly, you need to add this to your application: + +`MappingSchema.Default.SetConverter(dt => TimeOnly.FromDateTime(dt));` + +### Can I use protobuf-net? + +Yes. Add a dependency to protobuf-net and set a surrogate attribute: + +```c# +[ValueObject(typeof(string))] +[ProtoContract(Surrogate = typeof(string))] +public partial class BoxId { +//... +} +``` + +The BoxId type will now be serialized as a `string` in all messages and grpc calls. If one is generating `.proto` files +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_. diff --git a/docs/site/Writerside/topics/tutorials/Normalization.md b/docs/site/Writerside/topics/tutorials/Normalization.md index 99c5ddf198..6957a459d2 100644 --- a/docs/site/Writerside/topics/tutorials/Normalization.md +++ b/docs/site/Writerside/topics/tutorials/Normalization.md @@ -1,4 +1,4 @@ -# Clean up values +# Normalize input values This was requested in [this feature request](https://github.com/SteveDunn/Vogen/issues/80). @@ -18,7 +18,7 @@ namespace Vogen.Examples.TypicalScenarios // It cannot be empty, or start / end with whitespace. // We have a normalization method that first normalizes the string, then the // validation method that validates it. - [ValueObject(typeof(string))] + [ValueObject] public partial class ScrapedString { private static Validation Validate(string value) diff --git a/docs/site/Writerside/topics/tutorials/NormalizationTutorial.md b/docs/site/Writerside/topics/tutorials/NormalizationTutorial.md index 95ed97e7b6..4b7b2df2ad 100644 --- a/docs/site/Writerside/topics/tutorials/NormalizationTutorial.md +++ b/docs/site/Writerside/topics/tutorials/NormalizationTutorial.md @@ -1,3 +1,45 @@ -# Normalization user-provided values +# Normalizing user-provided values -## Coming soon \ No newline at end of file +In this tutorial, we'll see how to normalize (clean-up) input when creating value objects. + +We'll create a type that represents a vehicle registration plate in the UK. Vehicle registration plates vary from +country to country, but in the UK, they are all upper case, e.g. `AB12 XYZ`. + +Create a `VehicleRegistrationPlate` type: + + +```C# +[ValueObject] +public partial struct VehicleRegistrationPlate +{ +} +``` + +By default, creating one and using it does nothing with the value provided: + +```C# +var plate = VehicleRegistrationPlate.From("ab12 xyz"); +Console.WriteLine(plate); + +>> ab12 xyz +``` + +Now, add the following method: + +```C# +private static string NormalizeInput(string input) => + input.ToUpperInvariant(); +``` + +... and run it again to see that the data has been normalized as upper-case. + +```C# +var plate = VehicleRegistrationPlate.From("ab12 xyz"); +Console.WriteLine(plate); + +>> AB12 XYZ +``` + +Note that normalization happens before validation, so your method might get passed invalid raw data. + +Also note that if you [validate your values](Validation.md), then validation happens on the sanitized value. diff --git a/docs/site/Writerside/topics/tutorials/Serialization.md b/docs/site/Writerside/topics/tutorials/Serialization.md index b4865cfb04..64d9047616 100644 --- a/docs/site/Writerside/topics/tutorials/Serialization.md +++ b/docs/site/Writerside/topics/tutorials/Serialization.md @@ -1,3 +1,28 @@ # Serializing and persisting -## Coming soon \ No newline at end of file +By default, each VO is decorated with a `TypeConverter` and `System.Text.Json` (STJ) serializer. There are other converters/serializer for: + +* Newtonsoft.Json (NSJ) +* Dapper +* EFCore +* [LINQ to DB](https://github.com/linq2db/linq2db) +* protobuf-net (see the FAQ section below) + +They are controlled by the `Conversions` enum. The following has serializers for NSJ and STJ: + +```c# +[ValueObject(conversions: Conversions.NewtonsoftJson | Conversions.SystemTextJson, underlyingType: typeof(float))] +public readonly partial struct Celsius { } +``` + +If you don't want any conversions, then specify `Conversions.None`. + +If you want your own conversion, then again specify none, and implement them yourself, just like any other type. But be aware that even serializers will get the same compilation errors for `new` and `default` when trying to create VOs. + +If you want to use Dapper, remember to register it—something like this: + +```c# +SqlMapper.AddTypeHandler(new Customer.DapperTypeHandler()); +``` + +See the examples folder for more information. diff --git a/docs/site/Writerside/topics/tutorials/Validation.md b/docs/site/Writerside/topics/tutorials/Validation.md index a4f0189306..6523ff6ae7 100644 --- a/docs/site/Writerside/topics/tutorials/Validation.md +++ b/docs/site/Writerside/topics/tutorials/Validation.md @@ -4,4 +4,35 @@ Validate user-provided values -## Coming soon \ No newline at end of file +In this tutorial, we're going to extend the `CustomerId` type we created in +the [first tutorial](your-first-value-object.md). + +Vogen calls the `Validate` method when you create your types. Let's stop users from creating Customer IDs with +values of zero or less. + +Add the following `Validate` method to `CustomerId`: + +```C# +private static Validation Validate(int input) => input > 0 + ? Validation.Ok + : Validation.Invalid("Customer IDs must be greater than 0."); +``` + +Now, when you try to create one: + +```C# +var newCustomer = CustomerId.From(0); +``` + +you'll get an exception at runtime: +``` +Unhandled exception. Vogen.ValueObjectValidationException: + Customer IDs must be greater than 0. +``` + +Note that the method is `private` and `static`. +The method doesn't need to be `public` because it doesn't make sense for +anyone to call it; if something has a `CustomerId`, then **it is valid**, so users don't need to check the validity. +It is also `static` because it's not related to any particular _instance_ of this type. + +For more advanced scenarios, please see the [How-to article on validation](Validate-values.md) diff --git a/docs/site/Writerside/topics/tutorials/your-first-value-object.md b/docs/site/Writerside/topics/tutorials/your-first-value-object.md index 654dd08a8c..33e00bfc3b 100644 --- a/docs/site/Writerside/topics/tutorials/your-first-value-object.md +++ b/docs/site/Writerside/topics/tutorials/your-first-value-object.md @@ -4,32 +4,27 @@ Create and use your first Value Object -[Install](Installation.md) the package, and then create a new Value Object: - - - - - ] - public partial struct CustomerId { } - ]]> - - - - - - - - +In this tutorial, we'll create and use a Value Object. + +[Install](Installation.md) the package, and then, in your project, create a new Value Object that represents a Customer ID: + +```C# +[ValueObject] +public partial struct CustomerId { } +``` + +If you're not using generics, you can use `typeof` instead: + +```c# +[ValueObject(typeof(int))] +public partial struct CustomerId { } +``` -partial is required as the code generator augments this type by creating another partial class +the partial keyword is required as the code generator augments this type by creating another partial class -Create a new instance by using the `From` method: +Now, create a new instance by using the `From` method that the source generator generates: ```c# var customerId = CustomerId.From(42);