このプロジェクトが好きまたは使用している場合は、スターを渡してください。ありがとう!
Vogen( 「Voh Jen」と発音)は、.NETソースジェネレーターとアナライザーです。プリミティブ(ints、deicimalsなど)をドメインの概念(customerid、accountbalanceなど)を表す値オブジェクトに変えます。
新しいC#コンパイルエラーを追加して、無効な値オブジェクトの作成を停止するのに役立ちます。
このREADMEは、機能の一部をカバーしています。開始、チュートリアル、ハウツーなどの詳細情報については、Wikiをご覧ください。
ソースジェネレーターは、強く型付けされたドメインの概念を生成します。あなたはこれを提供します:
[ 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 ( ) .. .
}次に、有効で安全であることを完全に知るために、ドメイン内のintの代わりにCustomerIdを使用します。
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#コンパイルエラーの形で新しい制約を追加することで行います。初期化された値オブジェクトで終わる方法はいくつかあります。 1つの方法は、タイプのコンストラクターを提供することです。独自のコンストラクターを提供することは、値を設定するのを忘れていることを意味する可能性があるため、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.このプロジェクトの主な目標の1つは、プリミティブを直接使用するのとほぼ同じ速度とメモリのパフォーマンスを達成することです。別の言い方をすれば、 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 ; ソースジェネレーターは値オブジェクトを生成します。 Valueオブジェクトは、 int 、 string 、 doubleなどの単純なプリミティブを強くタイプのタイプで包むことにより、プリミティブな強迫観念と戦うのに役立ちます。
プリミティブな強迫観念(別名、ひどくタイプ)とは、プリミティブに夢中になっていることを意味します。ソフトウェアの品質を低下させるコードの臭いです。
「原始的な強迫観念は、プリミティブデータ型を使用してドメインのアイデアを表すことです」#
いくつかの例:
int ageの代わりに - 私たちはAge ageを持っているでしょう。 Age 、それが否定的ではない可能性があるという検証を持っているかもしれませんstring postcodeの代わりに - Postcode postcodeがあります。 Postcodeテキストの形式で検証があるかもしれませんソースジェネレーターは意見があります。意見は一貫性を確保するのに役立ちます。意見は次のとおりです。
Age.From(12) From名前が付けられた工場メソッドを介して構築されますAge.From(12) == Age.From(12) )Validation結果を返すValidateという名前の静的メソッドで検証されます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にあるため、事前にチェックされているかどうかはわかりません。そのため、使用するたびに確認する必要があります。それは原始的であるため、誰かが値を変更したかもしれないので、以前にチェックしたことがあると確信していても、もう一度チェックする必要があるかもしれません。
これまでのところ、例として、Value 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 * in the Vogen.Benchmarksフォルダー)
前述のように、Vogenの目標は、Primitives自身を使用することと比較して、非常によく似たパフォーマンスを達成することです。以下は、intネイティブ(原始的に?)を使用して、根本的なタイプのvsを使用して、検証済みの値オブジェクトの使用を比較したベンチマークです。
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 | - | - |
| balueObjectStructを使用します | 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 | 割り当てられます | アロック比 |
|---|---|---|---|---|---|---|---|---|
| usingstringnectivative | 151.8 ns | 32.19 | 1.76 | 1.00 | 0.00 | 0.0153 | 256 b | 1.00 |
| balueObjectAssTructを使用します | 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を指定してください。
独自のコンバージョンが必要な場合は、もう一度指定して、他のタイプと同じように自分で実装してください。ただし、SERIALIZERSでさえ、VOSを作成しようとすると、 new defaultで同じコンピレーションエラーが得られることに注意してください。
Dapperを使用したい場合は、登録することを忘れないでください - このようなもの:
SqlMapper . AddTypeHandler ( new Customer . DapperTypeHandler ( ) ) ;詳細については、例フォルダーを参照してください。
以下は、WikiのFAQページからの抜粋です。
はい、それはここにあります:https://stevedunn.github.io/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>
用語値オブジェクトは、平等がアイデンティティではなく値に基づいている小さなオブジェクトを表します。ウィキペディアから
コンピューターサイエンスでは、値オブジェクトは、等式がアイデンティティに基づいていない単純なエンティティを表す小さなオブジェクトです。つまり、2つの値オブジェクトは同じ値を持っている場合、必ずしも同じオブジェクトではない場合に等しくなります。
DDDでは、値オブジェクトは(再び、ウィキペディアから)です
...バリューオブジェクトは、属性を含むが概念的アイデンティティを持たない不変のオブジェクトです
public record struct CustomerId(int Value); ?それはあなたに検証を与えません。 Valueを検証するには、Shorthand構文(プライマリコンストラクター)を使用することはできません。だからあなたはする必要があるでしょう:
public record struct CustomerId
{
public CustomerId ( int value ) {
if ( value <= 0 ) throw new Exception ( .. . )
}
}また、データを検証しない可能性のある他のコンストラクターを提供し、それにより、ドメインに無効なデータを許可する場合があります。これらの他のコンストラクターは、例外を投げたり、異なる例外を投げたりすることはありません。 Vogenの意見の1つは、Value Objectに与えられた無効なデータが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ページをご覧ください。
[ValueObject<string>(conversions: Conversions.EfCoreValueConverter)] efcoreに、そのコンバーターをOnModelCreating方法で使用するように指示する必要があります。 builder . Entity < SomeEntity > ( b =>
{
b . Property ( e => e . Name ) . HasConversion ( new Name . EfCoreValueConverter ( ) ) ;
} ) ;できますが、ドメイン全体で一貫性を確保するには、どこでも検証する必要があります。そして、シャローの法律は、それは不可能だと言っています:
shallowayの法律「nが変化する必要があり、n> 1は、shallowayがこれらのもののせいぜいn -1で見つけるでしょう。」
具体的には、 「5つのものが変更する必要がある場合、シャロウェイはせいぜい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の複数の宣言のフィールド間に定義された順序はありません。
たとえば、入力をトリミングするなど、使用される値を正常化/消毒したいと思います。これは可能ですか?
はい、remormizeInputメソッドを追加します
private static string NormalizeInput ( string input ) => input . Trim ( ) ;詳細については、Wikiを参照してください。
もしそうならそれは素晴らしいことですが、現在ではありません。私はそれについての記事を書きましたが、要約すると、非定義不可能な価値の種類に焦点を当てた長年の言語提案があります。定義不可能な値タイプを持つことは素晴らしい第一歩ですが、検証を強制するために言語に何かを持っていることも便利です。そこで、不変の記録の言語提案を追加しました。
提案の回答の1つは、言語チームは、検証ポリシーがC#の一部ではなく、ソースジェネレーターによって提供されるべきであると判断したと述べています。
stringlytypedidこれはIDにより焦点を当てています。 Vogenは、「ドメインの概念」とそれらの概念に関連する制約に焦点を当てています。
ストライクされたこれは私の最初の試みであり、ソースで生成されていません。ベースタイプがクラスであるため、メモリオーバーヘッドがあります。アナライザーもありません。現在、Vogenを支持して非推奨としてマークされています。
Stringlythyped -source -sourceで生成され、アナライザーなしに似ています。これもよりリラックスしており、複合「基礎となる」タイプを可能にします。
Vogenに似たValueObjectGeneratorですが、検証に焦点が当てられておらず、コードアナライザーはありません。
任意のタイプをラップできます。シリアル化とタイプ変換には、次の実装があります。
弦
int
長さ
短い
バイト
フロート(シングル)
小数
ダブル
DateTime
DATEONLY
タイマリー
DateTimeOffset
ガイド
ブール
他のタイプの場合、汎用型変換とシリアイザーが適用されます。タイプ変換とシリアル化のために独自のコンバーターを供給している場合は、コンバーター用にNone指定し、独自のタイプの属性でタイプを飾ります。
[ ValueObject < SpecialPrimitive > ( conversions : Conversions . None ) ]
[ System . Text . Json . Serialization . JsonConverter ( typeof ( SpecialPrimitiveJsonConverter ) ) ]
[ System . ComponentModel . TypeConverter ( typeof ( SpecialPrimitiveTypeConverter ) ) ]
public partial struct SpecialMeasurement ;はい、 ValueObject属性のいずれかに例外タイプを指定するか、 VogenConfigurationを使用してグローバルに指定します。
DateTime TimeOnlyに変換することはできないというTimeOnlyをラップするValueObjectを使用すると、Linq2DBからエラーが発生します。どうすればよいですか? 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ファイルを生成している場合、ProtoファイルにはSulrogate Typeがタイプとして含まれます。この情報については@Domasmに感謝します。
貢献したすべての人々に感謝します!
Andrew LockのStronglytepedidから多くのインスピレーションを得ました。
また、GéraldBarreのMeziantou.Analyzerから素晴らしいアイデアもあります