如果您愿意或正在使用此项目,请给它星星。谢谢!
Vogen(发音为“ VOH Jen” )是.NET源发生器和分析仪。它将您的原始词(INT,小数等)变成代表域概念(客户ID,帐户平衡等)的价值对象
它添加了新的C#编译错误,以帮助停止创建无效的值对象。
此读数涵盖了一些功能。请参阅Wiki以获取更多详细信息,例如入门,教程和HowTOS。
源发电机生成强烈键入的域概念。您提供此:
[ ValueObject < int > ]
public partial struct CustomerId ;... vogen产生的来源类似:
public partial struct CustomerId :
System . IEquatable < CustomerId > ,
System . IComparable < CustomerId > , System . IComparable
{
private readonly int _value ;
public readonly int Value => _value ;
public CustomerId ( ) {
throw new Vogen . ValueObjectValidationException (
"Validation skipped by attempting to use the default constructor..." ) ;
}
private CustomerId ( int value ) => _value = value ;
public static CustomerId From ( int value ) {
CustomerId instance = new CustomerId ( value ) ;
return instance ;
}
public readonly bool Equals ( CustomerId other ) .. .
public readonly bool Equals ( int primitive ) .. .
public readonly override bool Equals ( object obj ) .. .
public static bool operator == ( CustomerId left , CustomerId right ) .. .
public static bool operator != ( CustomerId left , CustomerId right ) .. .
public static bool operator == ( CustomerId left , int right ) .. .
public static bool operator != ( CustomerId left , int right ) .. .
public static bool operator == ( int left , CustomerId right ) .. .
public static bool operator != ( int left , CustomerId right ) .. .
public readonly override int GetHashCode ( ) .. .
public readonly override string ToString ( ) .. .
}然后,您可以在域中使用CustomerId ,而不是在您的域中使用int ,以了解其有效且安全使用:
CustomerId customerId = CustomerId . From ( 123 ) ;
SendInvoice ( customerId ) ;
.. .
public void SendInvoice ( CustomerId customerId ) { .. . } int是值对象的默认类型。通常,要明确说明每种类型是一个好主意。您也可以(单独或全球)将它们配置为其他类型。请参阅文档稍后的配置部分。
这是其他一些例子:
[ ValueObject < decimal > ]
public partial struct AccountBalance ;
[ ValueObject < string > ]
public partial class LegalEntityName ;Vogen的主要目标是确保您的价值对象的有效性,代码分析仪可帮助您避免错误,这可能会使您域中的价值对象造成非直接化的价值对象。
它通过以新的C#编译错误的形式添加新约束来实现此目的。您可以通过几种方式使用非初始化的价值对象。一种方法是给您类型的构造函数。提供自己的构造函数可能意味着您忘记了设置一个值,因此Vogen不允许您拥有用户定义的构造函数:
[ 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
public CustomerId ( ) { }
// error VOG008: Cannot have user defined constructors,
// please use the From method for creation.
public CustomerId ( int value ) { }
}此外,在创建或消耗价值对象时,Vogen将发现问题:
// 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.
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
var c = Activator . CreateInstance < CustomerId > ( ) ; // error VOG025: Type 'CustomerId' cannot be constructed via Reflection as it is prohibited.
var c = Activator . CreateInstance ( < CustomerId > ) ; // error VOG025: Type 'MyVo' cannot be constructed via Reflection as
it is prohibited
// catches lambda expressions
Func < CustomerId > f = ( ) => default ; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
// 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 argument / parameter expressions
Task < CustomerId > t = Task . FromResult < CustomerId > ( new ( ) ) ; // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited
void Process ( CustomerId customerId = default ) { } // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.该项目的主要目标之一是达到与直接使用原始素的速度和内存性能几乎相同的速度和内存性能。换句话说,如果您的decimal原始代表帐户余额,则使用AccountBalance余额对象的开销极低。请参阅下面的性能指标。
Vogen是Nuget软件包。安装它:
dotnet add package Vogen
添加到项目中时,源生成器会为您的原始程序生成包装器,并且代码分析仪将使您知道是否尝试创建无效的值对象。
考虑一下您的域概念以及如何使用原始词来表示它们,例如,而不是:
public void HandlePayment ( int customerId , int accountId , decimal paymentAmount )...有这个:
public void HandlePayment ( CustomerId customerId , AccountId accountId , PaymentAmount paymentAmount )这就像创建这样的类型一样简单:
[ ValueObject ]
public partial struct CustomerId ;
[ ValueObject ]
public partial struct AccountId ;
[ ValueObject < decimal > ]
public partial struct PaymentAmount ; 源生成器生成值对象。价值对象通过包裹简单的原始图,例如int , string , double等,以强劲的类型来对抗原始的痴迷。
原始的痴迷(又名陷入困境)意味着要痴迷于原语。这是一种降低软件质量的代码气味。
“原始痴迷正在使用原始数据类型来表示域思想”#
一些例子:
int age - 我们的Age age 。 Age可能有验证,它不会是负面的string postcode - 我们将具有Postcode postcode 。 Postcode可能对文本格式有验证来源生成器是有用的。这些意见有助于确保一致性。意见是:
From “例如Age.From(12)Age.From(12) == Age.From(12) 。Validate ”返回Validation结果的静态方法验证Validation.Ok ValueObjectValidationException验证通常将领域的思想表示为原始,但是原语可能无法完全描述域的想法。
要使用价值对象而不是原始对象,我们只是简单地交换了这样的代码:
public class CustomerInfo {
private int _id ;
public CustomerInfo ( int id ) => _id = id ;
}..对此:
public class CustomerInfo {
private CustomerId _id ;
public CustomerInfo ( CustomerId id ) => _id = id ;
} 这里有一篇博客文章来描述它,但总结:
原始的痴迷正在痴迷于看似方便的方式,即诸如
ints和strings类的原语,使我们能够代表域对象和思想。
就是这样:
int customerId = 42那怎么了?
客户ID可能无法完全由int表示。 int可能为负或零,但不可能是客户ID。因此,我们对客户ID有限制。我们不能在int上表示或执行这些约束。
因此,我们需要一些验证,以确保满足客户ID的限制。因为它在int中,所以我们不能确定它是否已事先检查,因此我们需要每次使用它进行检查。因为它是一个原始的,所以有人可能已经改变了该值,因此,即使我们100%确定我们之前已经检查过它,它仍然可能需要再次检查。
到目前为止,我们以示例为示例,是价值42的客户ID。在C#中,“ 42 == 42 ”可能不足为奇(我没有在JavaScript中检查过! )。但是,在我们的域中, 42应该始终等于42吗?如果您将42的供应商ID与42的客户ID进行比较,则可能不会!但是原语不会在这里对您有所帮助(请记住, 42 == 42 !)。
( 42 == 42 ) // true
( SuppliedId . From ( 42 ) == SupplierId . From ( 42 ) ) // true
( SuppliedId . From ( 42 ) == VendorId . From ( 42 ) ) // compilation error但是有时,我们需要表示值对象无效或尚未设置。我们不希望在对象之外的任何人这样做,因为它可以意外使用。通常有Unspecified实例,例如
public class Person {
public Age Age { get ; } = Age . Unspecified ;
}我们可以使用一个Instance属性来做到这一点:
[ ValueObject ]
[ Instance ( "Unspecified" , - 1 ) ]
public readonly partial struct Age {
public static Validation Validate ( int value ) =>
value > 0 ? Validation . Ok : Validation . Invalid ( "Must be greater than zero." ) ;
}这会产生public static Age Unspecified = new Age(-1); 。构造函数是private ,因此只有这种类型才能(故意)创建无效的实例。
现在,当我们使用Age时,我们的验证变得更加清晰:
public void Process ( Person person ) {
if ( person . Age == Age . Unspecified ) {
// age not specified.
}
}我们还可以指定其他实例属性:
[ ValueObject < float > ]
[ Instance ( "Freezing" , 0 ) ]
[ Instance ( "Boiling" , 100 ) ]
public readonly partial struct Celsius {
public static Validation Validate ( float value ) =>
value >= - 273 ? Validation . Ok : Validation . Invalid ( "Cannot be colder than absolute zero" ) ;
} 每个值对象可以具有自己的可选配置。配置包括:
如果未指定上述任何一个,则将推断全局配置。看起来像这样:
[ assembly : VogenDefaults (
underlyingType : typeof ( int ) ,
conversions : Conversions . Default ,
throws : typeof ( ValueObjectValidationException ) ) ]这些再次是可选的。如果未指定,则将其默认为:
typeof(int)Conversions.Default ( TypeConverter and System.Text.Json )typeof(ValueObjectValidationException)有几个有关无效配置的代码分析警告,包括:
System.Exception派生的异常时。(要自己运行这些: dotnet run -c Release --framework net9.0 -- --job short --filter *在Vogen.Benchmarks文件夹中)
如前所述,与使用原始素本身相比,Vogen的目标是实现非常相似的性能。这是一个基准,将验证的值对象与基本类型的int vs使用INT进行了比较(原始的?)
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 | 方法 | 意思是 | 错误 | stddev | 比率 | 比率 | Gen0 | 分配 |
|---|---|---|---|---|---|---|---|
| 使用 | 14.55 ns | 1.443 ns | 0.079 ns | 1.00 | 0.00 | - | - |
| 使用ValueObjectStruct | 14.88 ns | 3.639 ns | 0.199 ns | 1.02 | 0.02 | - | - |
使用本机INT和VO结构之间没有明显的区别。两者在速度和内存方面几乎相同。
下一个最常见的情况是使用VO类代表本机String 。这些结果是:
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 | 方法 | 意思是 | 错误 | stddev | 比率 | 比率 | Gen0 | 分配 | 同种比率 |
|---|---|---|---|---|---|---|---|---|
| 用途 | 151.8 ns | 32.19 | 1.76 | 1.00 | 0.00 | 0.0153 | 256 b | 1.00 |
| 使用ValueObjectSasscruct | 184.8 ns | 12.19 | 0.67 | 1.22 | 0.02 | 0.0153 | 256 b | 1.00 |
开销的表现很小,但是这些测量值非常小。没有内存开销。
默认情况下,每个VO都用TypeConverter和System.Text.Json (STJ)序列化装饰。还有其他转换器/序列化器:
它们由Conversions枚举控制。以下具有NSJ和STJ的序列化:
[ ValueObject < float > ( conversions : Conversions . NewtonsoftJson | Conversions . SystemTextJson ) ]
public readonly partial struct Celsius ;如果您不需要任何转换,请指定Conversions.None 。
如果您想要自己的转换,请再次指定,并像其他任何类型一样自己实施。但是请注意,即使序列化器在尝试创建VO时也会遇到new和default的汇编错误。
如果您想使用dapper,请记住注册 - 类似的东西:
SqlMapper . AddTypeHandler ( new Customer . DapperTypeHandler ( ) ) ;有关更多信息,请参见示例文件夹。
接下来是Wiki的完整常见问题解答页面的摘录。
是的,在这里:https://stevedunn.github.io/vogen/vogen/vogen.html
源发电机为.NET标准2.0。它生成的代码支持从6.0开始和开始的所有C#语言版本
如果您在.NET框架项目中使用发电机并使用旧样式项目(“ SDK样式”项目之前的项目),那么您需要对几个事情进行不同的操作:
PackageReference添加参考: < ItemGroup >
< PackageReference Include = " Vogen " Version = " [LATEST_VERSION_HERE - E.G. 1.0.18] " PrivateAssets = " all " />
</ ItemGroup >latest (或其他8或更多): <PropertyGroup>
+ <LangVersion>latest</LangVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
术语值对象代表一个相等性的小对象是基于价值而不是身份。来自维基百科
在计算机科学中,值对象是一个小对象,它代表一个简单的实体,其平等不是基于身份:即两个值对象具有相同的值时相等,而不一定是相同的对象。
在DDD中,值对象是(再次,来自Wikipedia)
...值对象是一个不变的对象,包含属性,但没有概念身份
public record struct CustomerId(int Value); ?这不会给您验证。要验证Value ,您不能使用速记语法(主构造函数)。因此,您需要做:
public record struct CustomerId
{
public CustomerId ( int value ) {
if ( value <= 0 ) throw new Exception ( .. . )
}
}您可能还提供可能无法验证数据的其他构造函数,从而允许无效的数据进入您的域。那些其他构造函数可能不会引发例外,或者可能会引发不同的例外。 Vogen中的意见之一是,给出值对象的任何无效数据都会引发一个ValueObjectValidationException 。
您也可以使用default(CustomerId)来逃避验证。在Vogen中,有一些分析仪会抓住这一点并使构建失败,例如:
// 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 ) ;是的。默认情况下,每个VO都用TypeConverter和System.Text.Json (STJ)序列化装饰。还有其他转换器/序列化器:
是的,尽管有某些考虑。请参阅Wiki上的EFCORE页面,但是TL; DR是:
[ValueObject<string>(conversions: Conversions.EfCoreValueConverter)] ,您需要告诉efcore在OnModelCreating方法中使用该转换器,例如: builder . Entity < SomeEntity > ( b =>
{
b . Property ( e => e . Name ) . HasConversion ( new Name . EfCoreValueConverter ( ) ) ;
} ) ;您可以,但是为了确保整个域中的一致性,您必须在任何地方进行验证。浅水法则说,这是不可能的:
⚖️Alloway的律法“当n需要改变时,n> 1时,浅瓦最多会发现其中的n -1个东西。”
具体地: “当需要改变5件事时,Shalloway最多会发现其中的4件事。”
struct ,我可以禁止使用CustomerId customerId = default(CustomerId); ?是的。分析仪生成汇编误差。
struct ,我可以禁止使用CustomerId customerId = new(CustomerId); ?是的。分析仪生成汇编误差。
不。无参数构造函数是自动生成的,并且采用基础值的构造函数也将自动生成。
如果添加了更多的构造函数,那么您将从代码生成器中获得汇编错误,例如
[ ValueObject < 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 ) { }
}您可以,但是您会收到编译器警告CS0282-在部分类别或struct“ type”的多个声明中,在字段之间没有定义排序
我希望将所使用的值归一化/消毒,例如修剪输入。这可能吗?
是的,添加归一化方法,例如
private static string NormalizeInput ( string input ) => input . Trim ( ) ;有关更多信息,请参见Wiki。
如果是的话,那将是很棒的,但是目前不是。我写了一篇有关它的文章,但总而言之,有一个长期以来的语言建议,重点介绍了非违约价值类型。拥有不可默认的价值类型是一个很好的第一步,但是使用语言来实施验证的内容也很方便。因此,我为不变记录添加了语言建议。
提案中的一个答复之一说,语言团队决定验证政策不应是C#的一部分,而应由源生成器提供。
stronglypedpedID这更多地集中在ID上。 Vogen集中于更多的“领域概念”以及与这些概念相关的约束。
这是我的第一次尝试,并且不是来源生成的。由于基本类型是类,因此有内存开销。也没有分析仪。现在,它被标记为对vogen的弃用。
相似的价值类似于非源生成的且无分析仪。这也更加放松,并允许复合“基础”类型。
ValueObjectGenerator类似于Vogen,但较少专注于验证和无代码分析仪。
任何类型都可以包装。序列化和类型转换具有:
细绳
int
长的
短的
字节
浮子(单个)
十进制
双倍的
DateTime
dateonly
时间
dateTimeOffset
GUID
布尔
对于其他类型,应用了通用类型的转换和序列化器。如果您提供自己的转换器以进行类型的转换和序列化,请为转换器指定None并用自己类型的属性装饰您的类型,例如
[ ValueObject < SpecialPrimitive > ( conversions : Conversions . None ) ]
[ System . Text . Json . Serialization . JsonConverter ( typeof ( SpecialPrimitiveJsonConverter ) ) ]
[ System . ComponentModel . TypeConverter ( typeof ( SpecialPrimitiveTypeConverter ) ) ]
public partial struct SpecialMeasurement ;是的,通过指定VogenConfiguration的ValueObject属性中的异常类型。
TimeOnly提的值时,我会从linq2db出现一个错误,说DateTime不能转换为TimeOnly我该怎么办? LINQ2DB 4.0或更大的支持DateOnly和TimeOnly 。 Vogen生成了LINQ2DB的价值转换器;对于DateOnly ,它只是有效的,但是对于`timeonly,您需要将其添加到您的应用程序中:
MappingSchema.Default.SetConverter<DateTime, TimeOnly>(dt => TimeOnly.FromDateTime(dt));
是的。向Protobuf-net添加依赖关系,并设置一个替代属性:
[ ValueObject < string > ]
[ ProtoContract ( Surrogate = typeof ( string ) ) ]
public partial class BoxId {
//...
}BoxID类型现在将在所有消息/GRPC调用中序列化为字符串。如果一个人正在从C#代码生成其他应用程序的Proto文件,则原始文件将包括替代类型作为类型。感谢@domasm提供此信息。
感谢所有贡献的人!
我从安德鲁·洛克(Andrew Lock)的《强力杀伤力》中汲取了很多灵感。
我也从GéraldBarré的Meziantou.analyzer那里得到了一些很棒的想法