Se você gosta ou estiver usando este projeto, dê uma estrela. Obrigado!
Vogen ( pronunciado "Voh Jen" ) é um gerador de origem .NET e analisador. Ele transforma seus primitivos (INTs, decimais etc.) em objetos de valor que representam conceitos de domínio (CustomerID, AccountBalance etc.)
Ele adiciona novos erros de compilação C# para ajudar a interromper a criação de objetos de valor inválido.
Este readme cobre parte da funcionalidade. Consulte o wiki para obter informações mais detalhadas, como começar, tutoriais e instruções.
O gerador de origem gera conceitos de domínio fortemente digitados. Você fornece isso:
[ ValueObject < int > ]
public partial struct CustomerId ;... e Vogen gera fonte semelhante 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 ( ) .. .
} Em seguida, você usa CustomerId em vez de int em seu domínio no pleno conhecimento de que é válido e seguro de usar:
CustomerId customerId = CustomerId . From ( 123 ) ;
SendInvoice ( customerId ) ;
.. .
public void SendInvoice ( CustomerId customerId ) { .. . } int é o tipo padrão para objetos de valor. Geralmente, é uma boa idéia declarar explicitamente cada tipo de clareza. Você também pode - individualmente ou globalmente - configurá -los como outros tipos. Consulte a seção de configuração posteriormente no documento.
Aqui estão alguns outros exemplos:
[ ValueObject < decimal > ]
public partial struct AccountBalance ;
[ ValueObject < string > ]
public partial class LegalEntityName ;O principal objetivo do Vogen é garantir a validade dos seus objetos de valor , o analisador de código ajuda você a evitar erros, o que pode deixá -lo com objetos de valor não inicializado em seu domínio.
Isso é feito adicionando novas restrições na forma de novos erros de compilação C# . Existem algumas maneiras pelas quais você pode acabar com objetos de valor não inicializados. Uma maneira é dar aos seus construtores de tipos. Fornecer seus próprios construtores pode significar que você esquece de definir um valor, para que a Vogen não permita que você tenha construtores definidos pelo usuário :
[ 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 ) { }
}Além disso, a Vogen identificará problemas ao criar ou 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. Um dos principais objetivos deste projeto é alcançar quase a mesma velocidade e desempenho da memória que o uso diretamente das primitivas . Em outras palavras, se o seu primitivo decimal representar um saldo de conta, haverá uma sobrecarga extremamente baixa no uso de um objeto de valor AccountBalance . Por favor, veja as métricas de desempenho abaixo.
Vogen é um pacote NUGET. Instale -o com:
dotnet add package Vogen
Quando adicionado ao seu projeto, o gerador de origem gera os invólucros para suas primitivas e o analisador de código informará se você tentar criar objetos de valor inválido.
Pense nos seus conceitos de domínio e em como você usa primitivas para representá -los, por exemplo, em vez disso:
public void HandlePayment ( int customerId , int accountId , decimal paymentAmount )... tem isso:
public void HandlePayment ( CustomerId customerId , AccountId accountId , PaymentAmount paymentAmount )É tão simples quanto criar tipos como este:
[ ValueObject ]
public partial struct CustomerId ;
[ ValueObject ]
public partial struct AccountId ;
[ ValueObject < decimal > ]
public partial struct PaymentAmount ; O gerador de origem gera objetos de valor. Os objetos de valor ajudam a combater a obsessão primitiva, envolvendo primitivas simples, como int , string , double etc. em um tipo fortemente tipado.
A obsessão primitiva (também conhecida como estressante) significa ser obcecado por primitivas. É um cheiro de código que degrada a qualidade do software.
" Obsessão primitiva está usando tipos de dados primitivos para representar idéias de domínio " #
Alguns exemplos:
int age - teríamos Age age . Age pode ter validação de que não poderia ser negativostring postcode - teríamos Postcode postcode . Postcode pode ter validação no formato do textoO gerador de origem é opinativo. As opiniões ajudam a garantir a consistência. As opiniões são:
From , por exemplo, Age.From(12)Age.From(12) == Age.From(12) ))Validate que retorna um resultado ValidationValidation.Ok OK resulta em uma ValueObjectValidationException É comum representar idéias de domínio como primitivas, mas os primitivos podem não ser capazes de descrever completamente a idéia do domínio.
Para usar objetos de valor em vez de primitivos, simplesmente trocamos o código como este:
public class CustomerInfo {
private int _id ;
public CustomerInfo ( int id ) => _id = id ;
}.. para isso:
public class CustomerInfo {
private CustomerId _id ;
public CustomerInfo ( CustomerId id ) => _id = id ;
} Há um post do blog aqui que o descreve, mas resumir:
A obsessão primitiva está sendo obcecada com a maneira aparentemente conveniente de que os primitivos, como
intsestrings, nos permitem representar objetos e idéias de domínio.
É isso :
int customerId = 42O que há de errado nisso?
Um ID do cliente provavelmente não pode ser totalmente representado por um int . Um int pode ser negativo ou zero, mas é improvável que um ID do cliente possa ser. Portanto, temos restrições em um ID do cliente. Não podemos representar ou aplicar essas restrições a um int .
Portanto, precisamos de alguma validação para garantir que as restrições de um ID do cliente sejam atendidas. Como está no int , não podemos ter certeza se foi verificado com antecedência, por isso precisamos verificá -lo toda vez que o usamos. Por ser um primitivo, alguém pode ter mudado o valor; portanto, mesmo que tenhamos 100% de certeza de que o verificamos antes, ele ainda pode precisar verificar novamente.
Até agora, usamos como exemplo, um ID de cliente do valor 42 . Em C#, pode ser surpreendido que " 42 == 42 " ( eu não verifiquei isso no JavaScript! ). Mas, em nosso domínio , 42 sempre deve ser igual 42 ? Provavelmente não se você estiver comparando um ID de fornecedor de 42 a um ID de cliente de 42 ! Mas os primitivos não o ajudarão aqui (lembre -se, 42 == 42 !).
( 42 == 42 ) // true
( SuppliedId . From ( 42 ) == SupplierId . From ( 42 ) ) // true
( SuppliedId . From ( 42 ) == VendorId . From ( 42 ) ) // compilation error Mas, às vezes, precisamos denotar que um objeto de valor não seja válido ou não foi definido. Não queremos que ninguém fora do objeto faça isso, pois ele pode ser usado acidentalmente. É comum ter instâncias Unspecified , por exemplo
public class Person {
public Age Age { get ; } = Age . Unspecified ;
} Podemos fazer isso com um 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." ) ;
} Isso gera public static Age Unspecified = new Age(-1); . O construtor é private ; portanto, apenas esse tipo pode (deliberadamente) criar instâncias inválidas .
Agora, quando usamos Age , nossa validação fica mais clara:
public void Process ( Person person ) {
if ( person . Age == Age . Unspecified ) {
// age not specified.
}
}Também podemos especificar outras propriedades da instância:
[ 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 pode ter sua própria configuração opcional . A configuração inclui:
Se algum desses acima não for especificado, a configuração global será inferida. Parece o seguinte:
[ assembly : VogenDefaults (
underlyingType : typeof ( int ) ,
conversions : Conversions . Default ,
throws : typeof ( ValueObjectValidationException ) ) ]Aqueles são novamente opcionais. Se eles não são especificados, eles são padrão para:
typeof(int)Conversions.Default ( TypeConverter e System.Text.Json )typeof(ValueObjectValidationException)Existem vários avisos de análise de código para configuração inválida, incluindo:
System.Exception (Para executá -lo: dotnet run -c Release --framework net9.0 -- --job short --filter * na pasta Vogen.Benchmarks )
Como mencionado anteriormente, o objetivo do Vogen é alcançar um desempenho muito semelhante em comparação com o uso de primitivas. Aqui está uma referência comparando o uso de um objeto de valores validado com o tipo subjacente de int vs usando um denativo ( primitivo ?)
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 | Erro | Stddev | Razão | Razão | Gen0 | Alocado |
|---|---|---|---|---|---|---|---|
| Usando intrinhativamente | 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 | - | - |
Não há diferença discernível entre usar um INT nativo e uma estrutura de Vo; Ambos são praticamente os mesmos em termos de velocidade e memória.
O próximo cenário mais comum é usar uma classe VO para representar uma String nativa. Esses resultados são:
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 | Erro | Stddev | Razão | Razão | Gen0 | Alocado | Razão de alocação |
|---|---|---|---|---|---|---|---|---|
| Usingstringnativamente | 151,8 ns | 32.19 | 1.76 | 1,00 | 0,00 | 0,0153 | 256 b | 1,00 |
| UsandoValueObjectasstruct | 184,8 ns | 12.19 | 0,67 | 1.22 | 0,02 | 0,0153 | 256 b | 1,00 |
Há uma pequena quantidade de sobrecarga de desempenho, mas essas medições são incrivelmente pequenas. Não há sobrecarga de memória.
Por padrão, cada VO é decorado com um serializador TypeConverter e System.Text.Json (STJ). Existem outros conversores/serializador para:
Eles são controlados pela enumeração Conversions . O seguinte tem serializadores para NSJ e STJ:
[ ValueObject < float > ( conversions : Conversions . NewtonsoftJson | Conversions . SystemTextJson ) ]
public readonly partial struct Celsius ; Se você não quiser conversões, especifique Conversions.None .
Se você deseja sua própria conversão, especifique novamente nenhum e implemente -o você mesmo, como qualquer outro tipo. Mas esteja ciente de que mesmo os serializadores receberão os mesmos erros de compilação para new e default ao tentar criar o VOS.
Se você quiser usar o Dapper, lembre -se de registrá -lo - algo assim:
SqlMapper . AddTypeHandler ( new Customer . DapperTypeHandler ( ) ) ;Veja a pasta Exemplos para obter mais informações.
O que se segue é um trecho da página completa das perguntas frequentes no wiki.
Sim, está aqui: https://stevedunn.github.io/vogen/vogen.html
O gerador de origem é .NET Standard 2.0. O código que ele gera suporta todas as versões de idioma C# a partir de 6.0 e em diante
Se você estiver usando o gerador em um projeto .NET Framework e usando os projetos de estilo antigo (aquele antes dos projetos do 'estilo SDK'), precisará fazer algumas coisas de maneira diferente:
PackageReference no arquivo .csproj: < ItemGroup >
< PackageReference Include = " Vogen " Version = " [LATEST_VERSION_HERE - E.G. 1.0.18] " PrivateAssets = " all " />
</ ItemGroup >latest (ou qualquer coisa 8 ou mais): <PropertyGroup>
+ <LangVersion>latest</LangVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
O objeto de valor do termo representa um pequeno objeto cuja igualdade é baseada no valor e não na identidade. Da Wikipedia
Na ciência da computação, um objeto de valor é um pequeno objeto que representa uma entidade simples cuja igualdade não se baseia na identidade: ou seja, dois objetos de valor são iguais quando têm o mesmo valor, não necessariamente sendo o mesmo objeto.
No DDD, um objeto de valor é (novamente, da Wikipedia)
... Um objeto de valor é um objeto imutável que contém atributos, mas não tem identidade conceitual
public record struct CustomerId(int Value); ? Isso não lhe dá validação. Para validar Value , você não pode usar a sintaxe abreviada (construtor primário). Então você precisaria fazer:
public record struct CustomerId
{
public CustomerId ( int value ) {
if ( value <= 0 ) throw new Exception ( .. . )
}
} Você também pode fornecer outros construtores que podem não validar os dados, permitindo assim dados inválidos em seu domínio . Esses outros construtores podem não lançar exceção, ou podem lançar diferentes exceções. Uma das opiniões do Vogen é que quaisquer dados inválidos fornecidos a um objeto Value lança uma ValueObjectValidationException .
Você também pode usar default(CustomerId) para evitar a validação. No Vogen, existem analisadores que pegam isso e falham na construção, por exemplo:
// 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 ) ; Sim. Por padrão, cada VO é decorado com um serializador TypeConverter e System.Text.Json (STJ). Existem outros conversores/serializadores para:
Sim, embora existam certas considerações. Por favor, veja a página efcore no wiki, mas o tl; dr é:
[ValueObject<string>(conversions: Conversions.EfCoreValueConverter)] e precisa dizer à EFCore para usar esse conversor no método OnModelCreating , por exemplo: builder . Entity < SomeEntity > ( b =>
{
b . Property ( e => e . Name ) . HasConversion ( new Name . EfCoreValueConverter ( ) ) ;
} ) ;Você poderia, mas para garantir consistência em todo o seu domínio, teria que validar em todos os lugares . E a lei de Shallow diz que isso não é possível:
Lei Lei de Shalloway "Quando n as coisas precisam mudar e n> 1, o SHOULLAY encontrará no máximo n - 1 dessas coisas".
Concretamente: "Quando 5 coisas precisam mudar, o SHOULLAY encontrará no máximo, 4 dessas coisas".
struct , posso proibir o uso do CustomerId customerId = default(CustomerId); ?Sim . O analisador gera um erro de compilação.
struct , posso proibir o uso do CustomerId customerId = new(CustomerId); ?Sim . O analisador gera um erro de compilação.
Não . O construtor sem parâmetro é gerado automaticamente e o construtor que assume o valor subjacente também é gerado automaticamente.
Se você adicionar mais construtores, receberá um erro de compilação do gerador de código, por exemplo
[ 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 ) { }
}Você poderia , mas você receberá o aviso do compilador CS0282-não há pedidos definidos entre campos em múltiplas declarações de classe parcial ou estrutura 'tipo'
Gostaria de normalizar/higienizar os valores usados, por exemplo, aparar a entrada. Isso é possível?
Sim, adicione o método normalizado deput, por exemplo
private static string NormalizeInput ( string input ) => input . Trim ( ) ;Veja o Wiki para obter mais informações.
Seria ótimo se fosse, mas não é atualmente. Eu escrevi um artigo sobre isso, mas em resumo, há uma proposta de idioma de longa data focada em tipos de valor não defeituosos. Ter tipos de valor não defeituoso é um ótimo primeiro passo, mas também seria útil ter algo no idioma para aplicar o validador. Então, adicionei uma proposta de idioma para registros invariantes.
Uma das respostas da proposta diz que a equipe de idiomas decidiu que as políticas de validação não deveriam fazer parte do C#, mas fornecidas pelos geradores de origem.
Fortementepedid Isso está mais focado em IDs. A Vogen está mais focada em 'conceitos de domínio' e as restrições associadas a esses conceitos.
Strumingted, esta é a minha primeira tentativa e não é gerada por fonte. Há sobrecarga de memória porque o tipo de base é uma classe. Também não há analisadores. Agora está marcado como depreciado em favor do Vogen.
Valor de semelhante ao Strumingted - não gerado por fonte e nenhum analisador. Isso também é mais relaxado e permite tipos "subjacentes" compostos.
ValueObjectGenerator semelhante ao Vogen, mas menos focado na validação e nenhum analisador de código.
Qualquer tipo pode ser embrulhado. As conversões de serialização e tipo têm implementações para:
corda
int
longo
curto
byte
flutuar (único)
decimal
dobro
DateTime
DateOnly
TimeOnly
DateTimeOffset
Guid
bool
Para outros tipos, é aplicada uma conversão e serializador de tipo genérico. Se você estiver fornecendo seus próprios conversores para conversão de tipo e serialização, não especifique None para conversores e decore seu tipo com atributos para seus próprios tipos, por exemplo,
[ ValueObject < SpecialPrimitive > ( conversions : Conversions . None ) ]
[ System . Text . Json . Serialization . JsonConverter ( typeof ( SpecialPrimitiveJsonConverter ) ) ]
[ System . ComponentModel . TypeConverter ( typeof ( SpecialPrimitiveTypeConverter ) ) ]
public partial struct SpecialMeasurement ; Sim, especificando o tipo de exceção no atributo ValueObject , ou globalmente, com VogenConfiguration .
TimeOnly dizendo que DateTime não pode ser convertido em TimeOnly - o que devo fazer? Linq2db 4.0 ou superior suporta DateOnly e TimeOnly . O Vogen gera conversores de valor para LINQ2DB; Para DateOnly , ele apenas funciona, mas para `TimeOnly, você precisa adicionar isso ao seu aplicativo:
MappingSchema.Default.SetConverter<DateTime, TimeOnly>(dt => TimeOnly.FromDateTime(dt));
Sim. Adicione uma dependência ao Protobuf-Net e defina um atributo substituto:
[ ValueObject < string > ]
[ ProtoContract ( Surrogate = typeof ( string ) ) ]
public partial class BoxId {
//...
}O tipo BoxID agora será serializado como uma string em todas as mensagens/chamadas GRPC. Se alguém estiver gerando arquivos .proto para outros aplicativos a partir do código C#, os arquivos proto incluirão o tipo substituto como o tipo. Obrigado ao @DomaMasm por essas informações .
Obrigado a todas as pessoas que contribuíram!
Eu inspirei -me muito do Andrew Lock's fortlytypedIDID.
Eu também tenho ótimas idéias do meziantou.Analyzer de Gérald Barré