Si lo desea o está utilizando este proyecto, déle una estrella. ¡Gracias!
Vogen ( pronunciado "voh jen" ) es un generador y analizador de fuente .NET. Convierte sus primitivas (ints, decimales, etc.) en objetos de valor que representan conceptos de dominio (CustomerID, Cuentas, etc.)
Agrega nuevos errores de compilación de C# para ayudar a detener la creación de objetos de valor no válido.
Este readMe cubre parte de la funcionalidad. Consulte el wiki para obtener información más detallada, como comenzar, tutoriales y cómo.
El generador de origen genera conceptos de dominio fuertemente tipados. Usted proporciona esto:
[ ValueObject < int > ]
public partial struct CustomerId ;... y Vogen genera fuente similar a esta:
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 ( ) .. .
} Luego usa CustomerId en lugar de int en su dominio con el conocimiento completo de que es válido y seguro de usar:
CustomerId customerId = CustomerId . From ( 123 ) ;
SendInvoice ( customerId ) ;
.. .
public void SendInvoice ( CustomerId customerId ) { .. . } int es el tipo predeterminado para objetos de valor. En general, es una buena idea declarar explícitamente cada tipo de claridad. También puede, individual o globalmente, configurarlos para que sean otros tipos. Consulte la sección de configuración más adelante en el documento.
Aquí hay otros ejemplos:
[ ValueObject < decimal > ]
public partial struct AccountBalance ;
[ ValueObject < string > ]
public partial class LegalEntityName ;El objetivo principal de Vogen es garantizar la validez de sus objetos de valor , el analizador de código lo ayuda a evitar errores que pueden dejarlo con objetos de valor no inicializados en su dominio.
Lo hace agregando nuevas restricciones en forma de nuevos errores de compilación de C# . Hay algunas maneras en que podría terminar con objetos de valor no inicializados. Una forma es dando a sus constructores de tipo. Proporcionar sus propios constructores podría significar que olvida establecer un valor, por lo que Vogen no le permite tener constructores definidos por el usuario :
[ 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 ) { }
}Además, Vogen detectará problemas al crear o consumir objetos de valor:
// 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. Uno de los objetivos principales de este proyecto es lograr casi la misma velocidad y rendimiento de la memoria que usar primitivas directamente . Dicho de otra manera, si su primitivo decimal representa un saldo de cuenta, entonces hay una sobrecarga extremadamente baja de usar un objeto de valor AccountBalance . Consulte las métricas de rendimiento a continuación.
Vogen es un paquete Nuget. Instálelo con:
dotnet add package Vogen
Cuando se agrega a su proyecto, el generador de origen genera los envoltorios para sus primitivas y el analizador de código le informará si intenta crear objetos de valor no válido.
Piense en sus conceptos de dominio y en cómo usa primitivas para representarlas, por ejemplo, en lugar de esto:
public void HandlePayment ( int customerId , int accountId , decimal paymentAmount )... tener esto:
public void HandlePayment ( CustomerId customerId , AccountId accountId , PaymentAmount paymentAmount )Es tan simple como crear tipos como este:
[ ValueObject ]
public partial struct CustomerId ;
[ ValueObject ]
public partial struct AccountId ;
[ ValueObject < decimal > ]
public partial struct PaymentAmount ; El generador de origen genera objetos de valor. Los objetos de valor ayudan a combatir la obsesión primitiva envolviendo primitivas simples como int , string , double , etc. en un tipo fuertemente tipado.
La obsesión primitiva (también conocida como StringlyTyped) significa estar obsesionado con las primitivas. Es un olor a código que degrada la calidad del software.
" La obsesión primitiva está utilizando tipos de datos primitivos para representar ideas de dominio " #
Algunos ejemplos:
int age , tendríamos Age age . Age podría tener validación de que no podría ser negativostring postcode , tendríamos Postcode postcode . Postcode puede tener validación en el formato del textoEl generador de origen es obstinado. Las opiniones ayudan a garantizar la consistencia. Las opiniones son:
From Age.From(12)Age.From(12) == Age.From(12) .Validate que devuelve un resultado ValidationValidation.Ok da como resultado un ValueObjectValidationException que se está lanzando Es común representar ideas de dominio como primitivas, pero las primitivas podrían no ser capaces de describir completamente la idea de dominio.
Para usar objetos de valor en lugar de primitivas, simplemente intercambiamos un código así:
public class CustomerInfo {
private int _id ;
public CustomerInfo ( int id ) => _id = id ;
}.. a esto:
public class CustomerInfo {
private CustomerId _id ;
public CustomerInfo ( CustomerId id ) => _id = id ;
} Aquí hay una publicación de blog que lo describe, pero para resumir:
La obsesión primitiva se está obsesionando con la forma aparentemente conveniente de que las primitivas, como
intsystrings, nos permiten representar objetos e ideas de dominio.
Es esto :
int customerId = 42¿Qué pasa con eso?
Es probable que una identificación del cliente no pueda estar completamente representada por un int . Un int puede ser negativo o cero, pero es poco probable que una identificación del cliente pueda serlo. Entonces, tenemos limitaciones en una identificación del cliente. No podemos representar o hacer cumplir esas limitaciones en un int .
Por lo tanto, necesitamos alguna validación para garantizar que se cumplan las restricciones de una identificación del cliente. Debido a que está en int , no podemos estar seguros de si se ha marcado de antemano, por lo que debemos verificarlo cada vez que lo usamos. Debido a que es primitivo, alguien podría haber cambiado el valor, por lo que incluso si estamos 100% seguros de que lo hemos revisado antes, aún así podría necesitar verificar nuevamente.
Hasta ahora, hemos utilizado como ejemplo, una identificación del cliente de valor 42 . En C#, puede que no sea sorprendente que " 42 == 42 " ( ¡no lo he comprobado en JavaScript! ). Pero, en nuestro dominio , ¿ 42 siempre debería igualar 42 ? ¡Probablemente no si está comparando una ID de proveedor de 42 con una ID de cliente de 42 ! Pero las primitivas no te ayudarán aquí (recuerde, 42 == 42 !).
( 42 == 42 ) // true
( SuppliedId . From ( 42 ) == SupplierId . From ( 42 ) ) // true
( SuppliedId . From ( 42 ) == VendorId . From ( 42 ) ) // compilation error Pero a veces, debemos denotar que un objeto de valor no es válido o no se ha establecido. No queremos que nadie fuera del objeto haga esto, ya que podría usarse accidentalmente. Es común tener instancias Unspecified , por ejemplo
public class Person {
public Age Age { get ; } = Age . Unspecified ;
} Podemos hacer eso con un atributo 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." ) ;
} Esto genera public static Age Unspecified = new Age(-1); . El constructor es private , por lo que solo este tipo puede (deliberadamente) crear instancias no válidas .
Ahora, cuando usamos Age , nuestra validación se vuelve más clara:
public void Process ( Person person ) {
if ( person . Age == Age . Unspecified ) {
// age not specified.
}
}También podemos especificar otras propiedades de instancia:
[ 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" ) ;
} Cada objeto de valor puede tener su propia configuración opcional . La configuración incluye:
Si no se especifica alguno de los anteriores, entonces se infiere la configuración global. Se parece a esto:
[ assembly : VogenDefaults (
underlyingType : typeof ( int ) ,
conversions : Conversions . Default ,
throws : typeof ( ValueObjectValidationException ) ) ]Esos nuevamente son opcionales. Si no se especifican, entonces están predeterminados a:
typeof(int)Conversions.Default ( TypeConverter y System.Text.Json )typeof(ValueObjectValidationException)Hay varias advertencias de análisis de código para la configuración no válida, que incluyen:
System.Exception (Para ejecutarlos usted mismo: dotnet run -c Release --framework net9.0 -- --job short --filter * en la carpeta Vogen.Benchmarks )
Como se mencionó anteriormente, el objetivo de Vogen es lograr un rendimiento muy similar en comparación con el uso de primitivas. Aquí hay un punto de referencia que compara el uso de un objeto de valor validado con el tipo subyacente de INT vs utilizando una intencionada de forma nativa ( primitiva ?)
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étodo | Significar | Error | Stddev | Relación | Ratiosd | Gen0 | Asignado |
|---|---|---|---|---|---|---|---|
| Usando INTENTATIVO | 14.55 ns | 1.443 ns | 0.079 ns | 1.00 | 0.00 | - | - |
| Usando ValueObjectStruct | 14.88 ns | 3.639 ns | 0.199 ns | 1.02 | 0.02 | - | - |
No hay una diferencia discernible entre usar un int y una estructura VO nativa; Ambos son más o menos lo mismo en términos de velocidad y memoria.
El siguiente escenario más común es usar una clase VO para representar una String nativa. Estos resultados son:
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étodo | Significar | Error | Stddev | Relación | Ratiosd | Gen0 | Asignado | Asignación de la relación |
|---|---|---|---|---|---|---|---|---|
| Usatoria | 151.8 ns | 32.19 | 1.76 | 1.00 | 0.00 | 0.0153 | 256 b | 1.00 |
| UsandoValueObjectAstruct | 184.8 ns | 12.19 | 0.67 | 1.22 | 0.02 | 0.0153 | 256 b | 1.00 |
Hay una pequeña cantidad de sobrecarga de rendimiento, pero estas medidas son increíblemente pequeñas. No hay memoria de memoria.
Por defecto, cada VO está decorado con un serializador TypeConverter y System.Text.Json (STJ). Hay otros convertidores/serializador para:
Están controlados por las Conversions enum. Los siguientes tienen serializadores para NSJ y STJ:
[ ValueObject < float > ( conversions : Conversions . NewtonsoftJson | Conversions . SystemTextJson ) ]
public readonly partial struct Celsius ; Si no desea ninguna conversión, especifique Conversions.None .
Si desea su propia conversión, nuevamente especifique NINGUNO e impleméntelos usted mismo, como cualquier otro tipo. Pero tenga en cuenta que incluso los serializadores obtendrán los mismos errores de compilación para new y default al intentar crear VOS.
Si desea usar Dapper, recuerde registrarlo, algo como esto:
SqlMapper . AddTypeHandler ( new Customer . DapperTypeHandler ( ) ) ;Consulte la carpeta de ejemplos para obtener más información.
Lo que sigue es un extracto de la página de preguntas frecuentes completa en el wiki.
Sí, está aquí: https://stevedunn.github.io/vogen/vogen.html
El generador de origen es .NET Standard 2.0. El código que genera admite todas las versiones de idioma C# desde 6.0 y en adelante
Si está utilizando el generador en un proyecto .NET Framework y utilizando los proyectos de estilo antiguo (el anterior a los proyectos 'SDK Style'), entonces deberá hacer algunas cosas de manera diferente:
PackageReference en el archivo .csproj: < ItemGroup >
< PackageReference Include = " Vogen " Version = " [LATEST_VERSION_HERE - E.G. 1.0.18] " PrivateAssets = " all " />
</ ItemGroup >latest (o cualquier cosa 8 o más): <PropertyGroup>
+ <LangVersion>latest</LangVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
El objeto de valor de término representa un objeto pequeño cuya igualdad se basa en el valor y no en la identidad. De Wikipedia
En la informática, un objeto de valor es un objeto pequeño que representa una entidad simple cuya igualdad no se basa en la identidad: es decir, dos objetos de valor son iguales cuando tienen el mismo valor, no necesariamente son el mismo objeto.
En DDD, un objeto de valor es (nuevamente, de Wikipedia)
... un objeto de valor es un objeto inmutable que contiene atributos pero que no tiene identidad conceptual
public record struct CustomerId(int Value); ? Eso no te da validación. Para validar Value , no puede usar la sintaxis de taquigrafía (constructor primario). Entonces tendrías que hacer:
public record struct CustomerId
{
public CustomerId ( int value ) {
if ( value <= 0 ) throw new Exception ( .. . )
}
} También puede proporcionar otros constructores que podrían no validar los datos, lo que permite datos no válidos en su dominio . Esos otros constructores pueden no arrojar una excepción, o pueden arrojar diferentes excepciones. Una de las opiniones en Vogen es que cualquier datos no válidos dados a un objeto Value lanza un ValueObjectValidationException .
También puede usar default(CustomerId) para evadir la validación. En Vogen, hay analizadores que atrapan esto y fallan en la construcción, por ejemplo:
// 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 ) ; Sí. Por defecto, cada VO está decorado con un serializador TypeConverter y System.Text.Json (STJ). Hay otros convertidores/serializadores para:
Sí, aunque hay ciertas consideraciones. Consulte la página EFCORE en el wiki, pero el TL; DR es:
[ValueObject<string>(conversions: Conversions.EfCoreValueConverter)] y debe decirle a EFCORE que use ese convertidor en el método OnModelCreating , por ejemplo: builder . Entity < SomeEntity > ( b =>
{
b . Property ( e => e . Name ) . HasConversion ( new Name . EfCoreValueConverter ( ) ) ;
} ) ;Podría, pero para garantizar la consistencia en todo su dominio, tendría que validar en todas partes . Y la ley de Shallow dice que eso no es posible:
⚖️ Ley de Shalloway "Cuando n cosas necesitan cambiar y n> 1, Shalloway encontrará como máximo n - 1 de estas cosas".
Concretamente: "Cuando 5 cosas deben cambiar, Shalloway encontrará como máximo, 4 de estas cosas".
struct , ¿puedo prohibir el uso de CustomerId customerId = default(CustomerId); ?Sí . El analizador genera un error de compilación.
struct , ¿puedo prohibir el uso de CustomerId customerId = new(CustomerId); ?Sí . El analizador genera un error de compilación.
No . El constructor sin parámetros se genera automáticamente, y el constructor que toma el valor subyacente también se genera automáticamente.
Si agrega más constructores, obtendrá un error de compilación del generador de código, por ejemplo,
[ 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 ) { }
}Podrías , pero obtendrías advertencia de compilador CS0282: no hay ordenamiento definido entre campos en múltiples declaraciones de clase parcial o estructura 'tipo'
Me gustaría normalizar/desinfectar los valores utilizados, por ejemplo, recortando la entrada. ¿Es esto posible?
Sí, agregue el método de NormizeInput, por ejemplo
private static string NormalizeInput ( string input ) => input . Trim ( ) ;Vea Wiki para obtener más información.
Sería genial si lo fuera, pero no lo es actualmente. Escribí un artículo al respecto, pero en resumen, hay una propuesta de lenguaje de larga data que se centra en los tipos de valor no deformables. Tener tipos de valor no deformables es un gran primer paso, pero también sería útil tener algo en el idioma para imponer validar. Así que agregué una propuesta de idioma para registros invariantes.
Una de las respuestas en la propuesta dice que el equipo de idiomas decidió que las políticas de validación no deberían ser parte de C#, sino proporcionadas por los generadores de origen.
StronglyTypedid esto se centra más en las ID. Vogen se centra más en los 'conceptos de dominio' y las limitaciones asociadas con esos conceptos.
Stringlytyped este es mi primer intento y no es generado por la fuente. Hay una sobrecarga de memoria porque el tipo base es una clase. Tampoco hay analizadores. Ahora está marcado como desaprobado a favor del vogen.
Valor de similares a StringlyTyped: no generados por la fuente y sin analizadores. Esto también es más relajado y permite tipos compuestos 'subyacentes'.
ValueObjectGenerator similar a Vogen, pero menos centrado en la validación y sin analizador de código.
Cualquier tipo se puede envolver. La serialización y las conversiones de tipo tienen implementaciones para:
cadena
intencionalmente
largo
corto
byte
flotante (single)
decimal
doble
De fecha y hora
DateOnly
Tiempo
DateTimeOffset
Guía
bool
Para otros tipos, se aplica una conversión de tipo genérico y un serializador. Si está suministrando sus propios convertidores para la conversión de tipo y la serialización, entonces especifique None para los convertidores y decore su tipo con atributos para sus propios tipos, por ejemplo,
[ ValueObject < SpecialPrimitive > ( conversions : Conversions . None ) ]
[ System . Text . Json . Serialization . JsonConverter ( typeof ( SpecialPrimitiveJsonConverter ) ) ]
[ System . ComponentModel . TypeConverter ( typeof ( SpecialPrimitiveTypeConverter ) ) ]
public partial struct SpecialMeasurement ; Sí, especificando el tipo de excepción en el atributo ValueObject , o globalmente, con VogenConfiguration .
TimeOnly que diga que DateTime no se puede convertir en TimeOnly , ¿qué debo hacer? LINQ2DB 4.0 o mayor admite DateOnly y TimeOnly . Vogen genera convertidores de valor para LINQ2DB; Para DateOnly , simplemente funciona, pero para `TimeNnly, debe agregar esto a su aplicación:
MappingSchema.Default.SetConverter<DateTime, TimeOnly>(dt => TimeOnly.FromDateTime(dt));
Sí. Agregue una dependencia a ProtoBuf-Net y establezca un atributo sustituto:
[ ValueObject < string > ]
[ ProtoContract ( Surrogate = typeof ( string ) ) ]
public partial class BoxId {
//...
}El tipo de boxid ahora se serializará como una cadena en todas las llamadas de mensajes/GRPC. Si uno está generando archivos .proto para otras aplicaciones del código C#, los archivos Proto incluirán el tipo sustituto como el tipo. Gracias a @DOMASM por esta información .
¡Gracias a todas las personas que han contribuido!
Me inspiré mucho en Stronglytypedid de Andrew Lock.
También obtuve algunas ideas geniales de Meziandou de Géren Barré. Analyzer