如果您願意或正在使用此項目,請給它星星。謝謝!
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那裡得到了一些很棒的想法