Si vous aimez ou utilisez ce projet, veuillez lui donner une étoile. Merci!
Vogen ( prononcé "Voh Jen" ) est un générateur de source .NET et un analyseur. Il transforme vos primitives (INTS, décimales, etc.) en objets de valeur qui représentent les concepts de domaine (CustomerID, comptabilisation, etc.)
Il ajoute de nouvelles erreurs de compilation C # pour aider à arrêter la création d'objets de valeur non valide.
Cette lecture couvre une partie des fonctionnalités. Veuillez consulter le wiki pour des informations plus détaillées, telles que le démarrage, les tutoriels et les how-tos.
Le générateur source génère des concepts de domaine fortement typés. Vous fournissez ceci:
[ ValueObject < int > ]
public partial struct CustomerId ;... et Vogen génère une source similaire à ceci:
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 ( ) .. .
} Vous utilisez ensuite CustomerId au lieu de int dans votre domaine dans la pleine connaissance qu'il est valide et sûr à utiliser:
CustomerId customerId = CustomerId . From ( 123 ) ;
SendInvoice ( customerId ) ;
.. .
public void SendInvoice ( CustomerId customerId ) { .. . } int est le type par défaut pour les objets de valeur. C'est généralement une bonne idée de déclarer explicitement chaque type pour plus de clarté. Vous pouvez également - individuellement ou globalement - les configurer pour être d'autres types. Voir la section de configuration plus loin dans le document.
Voici quelques autres exemples:
[ ValueObject < decimal > ]
public partial struct AccountBalance ;
[ ValueObject < string > ]
public partial class LegalEntityName ;L'objectif principal de Vogen est d' assurer la validité de vos objets de valeur , l'analyseur de code vous aide à éviter les erreurs qui pourraient vous laisser des objets de valeur non initialisés dans votre domaine.
Il le fait en ajoutant de nouvelles contraintes sous la forme de nouvelles erreurs de compilation C # . Il existe plusieurs façons de vous retrouver avec des objets de valeur non initialisés. Une façon consiste à donner à vos constructeurs de type. Fournir vos propres constructeurs pourrait signifier que vous oubliez de définir une valeur, donc Vogen ne vous permet pas d'avoir des constructeurs définis par l'utilisateur :
[ 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 ) { }
}De plus, Vogen repérera des problèmes lors de la création ou de la consommation d'objets de valeur:
// 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. L'un des principaux objectifs de ce projet est d'atteindre presque la même vitesse et les mêmes performances de mémoire que l'utilisation des primitives directement . Mettez une autre façon, si votre decimal primitive représente un solde de compte, il y a plutôt des frais généraux extrêmement faibles d'utilisation d'un objet de valeur AccountBalance . Veuillez consulter les mesures de performance ci-dessous.
Vogen est un ensemble NuGet. Installez-le avec:
dotnet add package Vogen
Lorsqu'elle est ajoutée à votre projet, le générateur source génère les emballages pour vos primitives et l' analyseur de code vous permettra de savoir si vous essayez de créer des objets de valeur non valides.
Pensez à vos concepts de domaine et à la façon dont vous utilisez des primitives pour les représenter, par exemple au lieu de ceci:
public void HandlePayment ( int customerId , int accountId , decimal paymentAmount )... avoir ceci:
public void HandlePayment ( CustomerId customerId , AccountId accountId , PaymentAmount paymentAmount )C'est aussi simple que de créer des types comme ceci:
[ ValueObject ]
public partial struct CustomerId ;
[ ValueObject ]
public partial struct AccountId ;
[ ValueObject < decimal > ]
public partial struct PaymentAmount ; Le générateur source génère des objets de valeur. Les objets de valeur aident à lutter contre l'obsession primitive en emballage des primitives simples telles que int , string , double etc. dans un type fortement typé.
L'obsession primitive (alias Terly-Typed) signifie être obsédée par les primitives. C'est une odeur de code qui dégrade la qualité des logiciels.
" L'obsession primitive consiste à utiliser des types de données primitifs pour représenter les idées de domaine " #
Quelques exemples:
int age - nous aurions Age age . Age pourrait avoir une validation que cela ne pourrait pas être négatifstring postcode - nous aurions Postcode postcode . Postcode pourrait avoir une validation sur le format du texteLe générateur source est opiniâtre. Les opinions aident à assurer la cohérence. Les opinions sont:
From exemple, Age.From(12)Age.From(12) == Age.From(12) )Validate qui renvoie un résultat ValidationValidation.Ok ValueObjectValidationException Il est courant de représenter les idées de domaine comme des primitives, mais les primitives pourraient ne pas être en mesure de décrire pleinement l'idée du domaine.
Pour utiliser des objets de valeur au lieu des primitives, nous échangeons simplement du code comme ceci:
public class CustomerInfo {
private int _id ;
public CustomerInfo ( int id ) => _id = id ;
}.. à ceci:
public class CustomerInfo {
private CustomerId _id ;
public CustomerInfo ( CustomerId id ) => _id = id ;
} Il y a un article de blog ici qui le décrit, mais pour résumer:
L'obsession primitive est obsédée par la manière apparemment pratique que les primitives, telles que
intsetstrings, nous permettent de représenter des objets et des idées de domaine.
C'est ceci :
int customerId = 42Quel est le problème avec ça?
Un ID client ne peut probablement pas être entièrement représenté par un int . Un int peut être négatif ou zéro, mais il est peu probable qu'un ID client puisse l'être. Nous avons donc des contraintes sur un identifiant client. Nous ne pouvons pas représenter ou appliquer ces contraintes sur un int .
Nous avons donc besoin d'une certaine validation pour garantir que les contraintes d'un ID client sont respectées. Parce que c'est dans int , nous ne pouvons pas être sûrs s'il a été vérifié à l'avance, nous devons donc le vérifier à chaque fois que nous l'utilisons. Parce que c'est une primitive, quelqu'un a peut-être changé la valeur, donc même si nous sommes sûrs à 100% que nous l'avons déjà vérifié, il pourrait encore avoir besoin de vérifier à nouveau.
Jusqu'à présent, nous avons utilisé comme exemple, un ID client de valeur 42 . En C #, il n'est pas surprenant que " 42 == 42 " ( je n'ai pas vérifié cela en javascript! ). Mais, dans notre domaine , 42 devrait-il toujours égaler 42 ? Probablement pas si vous comparez un ID de fournisseur de 42 à un ID client de 42 ! Mais les primitives ne vous aideront pas ici (rappelez-vous, 42 == 42 !).
( 42 == 42 ) // true
( SuppliedId . From ( 42 ) == SupplierId . From ( 42 ) ) // true
( SuppliedId . From ( 42 ) == VendorId . From ( 42 ) ) // compilation error Mais parfois, nous devons indiquer qu'un objet de valeur n'est pas valide ou n'a pas été défini. Nous ne voulons que quiconque en dehors de l'objet faisait cela car il pourrait être utilisé accidentellement. Il est courant d'avoir des instances Unspecified , par exemple
public class Person {
public Age Age { get ; } = Age . Unspecified ;
} Nous pouvons le faire avec un attribut 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." ) ;
} Cela génère public static Age Unspecified = new Age(-1); . Le constructeur est private , donc seul ce type peut (délibérément) créer des instances non valides .
Maintenant, lorsque nous utilisons Age , notre validation devient plus claire:
public void Process ( Person person ) {
if ( person . Age == Age . Unspecified ) {
// age not specified.
}
}Nous pouvons également spécifier d'autres propriétés d'instance:
[ 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" ) ;
} Chaque objet de valeur peut avoir sa propre configuration facultative . La configuration comprend:
Si l'un de ceux ci-dessus n'est pas spécifié, la configuration globale est déduite. Cela ressemble à ceci:
[ assembly : VogenDefaults (
underlyingType : typeof ( int ) ,
conversions : Conversions . Default ,
throws : typeof ( ValueObjectValidationException ) ) ]Ceux-ci sont à nouveau facultatifs. S'ils ne sont pas spécifiés, ils sont par défaut:
typeof(int)Conversions.Default ( TypeConverter et System.Text.Json )typeof(ValueObjectValidationException)Il existe plusieurs avertissements d'analyse de code pour la configuration non valide, notamment:
System.Exception (Pour les exécuter vous-même: dotnet run -c Release --framework net9.0 -- --job short --filter * dans le dossier Vogen.Benchmarks )
Comme mentionné précédemment, l'objectif de Vogen est d'obtenir des performances très similaires par rapport à l'utilisation des primitives elles-mêmes. Voici une référence comparant l'utilisation d'un objet de valeur validé avec un type sous-jacent INT VS en utilisant un int native ( primitivement ?)
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 | Méthode | Signifier | Erreur | Stddev | Rapport | Ratiosd | Gen0 | Alloué |
|---|---|---|---|---|---|---|---|
| Utiliser Intrnativement | 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 | - | - |
Il n'y a pas de différence perceptible entre l'utilisation d'un int natif et une structure VO; Les deux sont à peu près les mêmes en termes de vitesse et de mémoire.
Le prochain scénario le plus courant consiste à utiliser une classe VO pour représenter une String native. Ces résultats sont:
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 | Méthode | Signifier | Erreur | Stddev | Rapport | Ratiosd | Gen0 | Alloué | Ratio alloc |
|---|---|---|---|---|---|---|---|---|
| UsingstringNativement | 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 |
Il y a une infime quantité de frais généraux de performance, mais ces mesures sont incroyablement petites. Il n'y a pas de surcharge de mémoire.
Par défaut, chaque VO est décoré d'un sérialiseur TypeConverter et System.Text.Json (STJ). Il existe d'autres convertisseurs / sérialiseurs pour:
Ils sont contrôlés par l'énumération Conversions . Ce qui suit a des sérialiseurs pour NSJ et STJ:
[ ValueObject < float > ( conversions : Conversions . NewtonsoftJson | Conversions . SystemTextJson ) ]
public readonly partial struct Celsius ; Si vous ne voulez pas de conversions, spécifiez Conversions.None .
Si vous voulez votre propre conversion, ne spécifiez pas encore aucune et implémentez-les vous-même, comme tout autre type. Mais sachez que même les sérialiseurs obtiendront les mêmes erreurs de compilation pour new et default lors de la création de VOS.
Si vous souhaitez utiliser Dapper, n'oubliez pas de l'enregistrer - quelque chose comme ceci:
SqlMapper . AddTypeHandler ( new Customer . DapperTypeHandler ( ) ) ;Voir le dossier Exemples pour plus d'informations.
Ce qui suit est un extrait de la page FAQ complète du wiki.
Oui, c'est ici: https://stevedunn.github.io/vogen/vogen.html
Le générateur source est .NET Standard 2.0. Le code qu'il génère prend en charge toutes les versions linguistiques C # à partir de 6.0 et
Si vous utilisez le générateur dans un projet .NET Framework et que vous utilisez les projets à ancienne style (celui avant les projets «style SDK»), vous devrez faire quelques choses différemment:
PackageReference dans le fichier .csproj: < ItemGroup >
< PackageReference Include = " Vogen " Version = " [LATEST_VERSION_HERE - E.G. 1.0.18] " PrivateAssets = " all " />
</ ItemGroup >latest (ou tout 8 ou plus): <PropertyGroup>
+ <LangVersion>latest</LangVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
L'objet de valeur à terme représente un petit objet dont l'égalité est basée sur la valeur et non l'identité. De Wikipedia
En informatique, un objet de valeur est un petit objet qui représente une entité simple dont l'égalité n'est pas basée sur l'identité: c'est-à-dire que deux objets de valeur sont égaux lorsqu'ils ont la même valeur, pas nécessairement le même objet.
Dans DDD, un objet de valeur est (encore une fois, de Wikipedia)
... un objet de valeur est un objet immuable qui contient des attributs mais n'a pas d'identité conceptuelle
public record struct CustomerId(int Value); ? Cela ne vous donne pas la validation. Pour valider Value , vous ne pouvez pas utiliser la syntaxe du raccourci (constructeur principal). Vous devriez donc faire:
public record struct CustomerId
{
public CustomerId ( int value ) {
if ( value <= 0 ) throw new Exception ( .. . )
}
} Vous pouvez également fournir d'autres constructeurs qui pourraient ne pas valider les données, permettant ainsi des données non valides dans votre domaine . Ces autres constructeurs pourraient ne pas lancer une exception ou donner des exceptions différentes. L'une des opinions de Vogen est que toutes les données non valides données à un objet de valeur lance une ValueObjectValidationException .
Vous pouvez également utiliser default(CustomerId) pour échapper à la validation. Dans Vogen, il y a des analyseurs qui captent cela et échouent à la construction, par exemple:
// 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 ) ; Oui. Par défaut, chaque VO est décoré d'un sérialiseur TypeConverter et System.Text.Json (STJ). Il existe d'autres convertisseurs / sérialiseurs pour:
Oui, bien qu'il y ait certaines considérations. Veuillez consulter la page Efcore sur le wiki, mais le TL; DR est:
[ValueObject<string>(conversions: Conversions.EfCoreValueConverter)] et vous devez dire à Efcore d'utiliser ce convertisseur dans la méthode OnModelCreating , par exemple: builder . Entity < SomeEntity > ( b =>
{
b . Property ( e => e . Name ) . HasConversion ( new Name . EfCoreValueConverter ( ) ) ;
} ) ;Vous pouvez, mais pour assurer la cohérence dans votre domaine, vous devrez valider partout . Et la loi de Shallow dit que ce n'est pas possible:
⚖️ La loi de Shalloway "quand n les choses doivent changer et n> 1, Shalloway trouvera au plus n - 1 de ces choses".
Concrètement: "Quand 5 choses doivent changer, Shalloway trouvera au plus 4 de ces choses."
struct , puis-je interdire l'utilisation de CustomerId customerId = default(CustomerId); ?Oui . L'analyseur génère une erreur de compilation.
struct , puis-je interdire l'utilisation de CustomerId customerId = new(CustomerId); ?Oui . L'analyseur génère une erreur de compilation.
Non . Le constructeur sans paramètre est généré automatiquement et le constructeur qui prend la valeur sous-jacent est également généré automatiquement.
Si vous ajoutez d'autres constructeurs, vous obtiendrez une erreur de compilation du générateur de code, par exemple
[ 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 ) { }
}Vous pourriez , mais vous obtiendrez un avertissement de compilateur CS0282 - il n'y a pas d'ordre défini entre les champs dans plusieurs déclarations de classe partielle ou de structure 'type'
Je voudrais normaliser / désinfecter les valeurs utilisées, par exemple, la réduction de l'entrée. Est-ce possible?
Oui, ajoutez la méthode de normalisation en point, par exemple
private static string NormalizeInput ( string input ) => input . Trim ( ) ;Voir Wiki pour plus d'informations.
Ce serait formidable si c'était le cas, mais ce n'est pas le cas actuellement. J'ai écrit un article à ce sujet, mais en résumé, il existe une proposition de langue de longue date axée sur les types de valeur non défautables. Le fait d'avoir des types de valeur non défautables est une excellente première étape, mais il serait également pratique d'avoir quelque chose dans la langue pour appliquer le validation. J'ai donc ajouté une proposition de langue pour les enregistrements invariants.
L'une des réponses de la proposition indique que l'équipe linguistique a décidé que les politiques de validation ne devraient pas faire partie de C #, mais fournies par les générateurs de source.
StronglyTypeded cela est davantage axé sur les identifiants. Vogen se concentre davantage de «concepts de domaine» et les contraintes associées à ces concepts.
Terly-Typed, c'est ma première tentative et est non généré par la source. Il y a des frais généraux de mémoire car le type de base est une classe. Il n'y a pas non plus d'analyseurs. Il est désormais marqué comme déprécié en faveur de Vogen.
Valeur de similaire à Stally Typed - non généré par la source et sans analyseurs. Ceci est également plus détendu et permet des types composites «sous-jacents».
ValueObjectGenerator similaire à Vogen, mais moins axé sur la validation et aucun analyseur de code.
Tout type peut être enveloppé. Les conversions de sérialisation et de type ont des implémentations pour:
chaîne
int
long
court
octet
Float (célibataire)
décimal
double
DateTime
Datant
À temps
DateTimeOffset
Guidage
bool
Pour d'autres types, une conversion et un sérialiseur génériques sont appliqués. Si vous fournissez vos propres convertisseurs pour la conversion et la sérialisation de type, alors ne spécifiez None pour les convertisseurs et décorez votre type avec des attributs pour vos propres types, par exemple
[ ValueObject < SpecialPrimitive > ( conversions : Conversions . None ) ]
[ System . Text . Json . Serialization . JsonConverter ( typeof ( SpecialPrimitiveJsonConverter ) ) ]
[ System . ComponentModel . TypeConverter ( typeof ( SpecialPrimitiveTypeConverter ) ) ]
public partial struct SpecialMeasurement ; Oui, en spécifiant le type d'exception dans l'attribut ValueObject , ou dans le monde, avec VogenConfiguration .
TimeOnly que DateTime ne peut pas être converti en TimeOnly - que dois-je faire? Linq2DB 4.0 ou plus prend en charge DateOnly et TimeOnly . Vogen génère des convertisseurs de valeur pour LINQ2DB; Pour DateOnly , cela fonctionne simplement, mais pour `` `` `` `` `` `'les choses, vous devez l'ajouter à votre application:
MappingSchema.Default.SetConverter<DateTime, TimeOnly>(dt => TimeOnly.FromDateTime(dt));
Oui. Ajoutez une dépendance à Protobuf-Net et définissez un attribut de substitution:
[ ValueObject < string > ]
[ ProtoContract ( Surrogate = typeof ( string ) ) ]
public partial class BoxId {
//...
}Boxid Type sera maintenant sérialisé en tant que chaîne dans tous les messages / appels GRPC. Si l'on génère des fichiers .proto pour d'autres applications à partir du code C #, les fichiers Proto incluront le type de substitution comme type. Merci à @Domasm pour ces informations .
Merci à toutes les personnes qui ont contribué!
Je me suis inspiré beaucoup de la forteté d'Andrew Lock.
J'ai aussi eu de grandes idées de meziantou de Gérald Barré.