Если вы любите или используете этот проект, пожалуйста, дайте ему звезду. Спасибо!
Vogen ( произносится «Voh Jen» ) - это генератор источника и анализатора .NET. Он превращает ваши примитивы (Ints, Decimals и т. Д.) В объекты стоимости, которые представляют концепции домена (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 ( ) .. .
} Затем вы используете 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 , который возвращает результат ValidationValidation.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Что не так с этим?
Идентификатор клиента, вероятно, не может быть полностью представлен int . int может быть отрицательным или нулевым, но вряд ли может быть идентификатор клиента. Итак, у нас есть ограничения на идентификатор клиента. Мы не можем представлять или обеспечить соблюдение этих ограничений на int .
Таким образом, нам нужна некоторая проверка, чтобы обеспечить соблюдение ограничений идентификатора клиента. Поскольку это в int , мы не можем быть уверены, что он был проверен заранее, поэтому нам нужно проверять это каждый раз, когда мы его используем. Поскольку это примитив, кто -то, возможно, изменил бы значение, поэтому даже если мы на 100% уверены, что мы проверили его раньше, это все равно может потребоваться снова.
До сих пор мы использовали в качестве примера, идентификатор клиента значения 42 . В C#может неудивительно, что « 42 == 42 » ( я не проверил это в JavaScript! ). Но в нашем домене 42 должны всегда равняться 42 ? Вероятно, нет, если вы сравниваете идентификатор поставщика 42 с идентификатором клиента 42 ! Но примитивы вам здесь не помогут (помните, 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" ) ;
} Каждый объект Value может иметь свою собственную необязательную конфигурацию. Конфигурация включает в себя:
Если какое -либо из указанных выше не указано, то глобальная конфигурация выводится. Похоже, это:
[ assembly : VogenDefaults (
underlyingType : typeof ( int ) ,
conversions : Conversions . Default ,
throws : typeof ( ValueObjectValidationException ) ) ]Они снова необязательны. Если они не указаны, то они дефоруются:
typeof(int)Conversions.Default ( TypeConverter и System.Text.Json )typeof(ValueObjectValidationException)Есть несколько предупреждений о анализе кода для неверной конфигурации, включая:
System.Exception Vogen.Benchmarks Чтобы запустить их самостоятельно: dotnet run -c Release --framework net9.0 -- --job short --filter *
Как упоминалось ранее, целью Vogen является достижение очень похожей производительности по сравнению с использованием самих примитивов. Вот эталон, сравнивающий использование проверенного объекта значения с базовым типом int против, используя int notical ( примитивно ?)
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 нс | 1,443 нс | 0,079 нс | 1,00 | 0,00 | - | - |
| Использование ValueObjectStruct | 14,88 нс | 3.639 нс | 0,199 нс | 1.02 | 0,02 | - | - |
Нет заметной разницы между использованием нативного int и vo struct; Оба почти одинаковы с точки зрения скорости и памяти.
Следующим наиболее распространенным сценарием является использование класса 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 | Выделено | Коэффициент выделения |
|---|---|---|---|---|---|---|---|---|
| USINGSTRINGNALY | 151,8 нс | 32.19 | 1.76 | 1,00 | 0,00 | 0,0153 | 256 б | 1,00 |
| Использование ValueObjectAssTruct | 184,8 нс | 12.19 | 0,67 | 1.22 | 0,02 | 0,0153 | 256 б | 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 .
Если вам нужна собственная конверсия, то снова укажите ни одного и реализуйте их самостоятельно, как и любой другой тип. Но имейте в виду, что даже сериализаторы получат те же ошибки компиляции для new и default при попытке создать VOS.
Если вы хотите использовать Dapper, не забудьте зарегистрировать его - что -то вроде этого:
SqlMapper . AddTypeHandler ( new Customer . DapperTypeHandler ( ) ) ;Смотрите папку «Примеры» для получения дополнительной информации.
Далее следует отрыв из полной страницы часто задаваемых вопросов в вики.
Да, это здесь: https://stevedunn.github.io/vogen/vogen.html
Генератор источника является .NET STANDAL 2.0. Код, который он генерирует, поддерживает все языковые версии C# с 6.0 и далее
Если вы используете генератор в проекте .NET Framework и используете проекты старого стиля (тот, который перед проектами «SDK Style»), вам нужно делать несколько вещей по -другому:
PackageReference в файле .csproj: < 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>
Объект Term Value представляет небольшой объект, который равенство, основано на стоимости, а не идентичности. Из Википедии
В информатике объект Value - это небольшой объект, который представляет простую сущность, равенство которого не основано на идентичности: т.е. два объекта значения равны, когда они имеют одинаковое значение, не обязательно является одним и тем же объектом.
В DDD объект значения (опять же, из Википедии)
... объект значения - это неизменная объект, который содержит атрибуты, но не имеет концептуальной идентичности
public record struct CustomerId(int Value); ? Это не дает вам подтверждения. Чтобы подтвердить Value , вы не можете использовать синтаксис сокращения (первичный конструктор). Так что вам нужно сделать:
public record struct CustomerId
{
public CustomerId ( int value ) {
if ( value <= 0 ) throw new Exception ( .. . )
}
} Вы также можете предоставить другие конструкторы, которые могут не проверять данные, тем самым позволяя неверные данные в ваш домен . Эти другие конструкторы могут не бросить исключение или могут сделать разные исключения. Одним из мнений в Vogen является то, что любые недопустимые данные, предоставленные объекту Value, бросают значение 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). Есть и другие конвертеры/сериализаторы для:
Да, хотя есть определенные соображения. Пожалуйста, посмотрите страницу EFCore на вики, но TL; DR есть:
OnModelCreating например, [ValueObject<string>(conversions: Conversions.EfCoreValueConverter)] builder . Entity < SomeEntity > ( b =>
{
b . Property ( e => e . Name ) . HasConversion ( new Name . EfCoreValueConverter ( ) ) ;
} ) ;Вы могли бы, но чтобы обеспечить согласованность на протяжении всего домена, вам придется проверять повсюду . И закон Шалова говорит, что это невозможно:
⚖ Закон Celloway «Когда N должен измениться, а n> 1, Challoway найдет не более N - 1 из этих вещей».
Конкретно: «Когда 5 вещей должны измениться, Felloway найдет больше всего, 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-Там нет определенного упорядочения между полями во множественных объявлениях частичного класса или структуры 'типа'
Я хотел бы нормализовать/дезинфицировать используемые значения, например, обрезать вход. Это возможно?
Да, добавьте метод NormalizeInput, например,
private static string NormalizeInput ( string input ) => input . Trim ( ) ;См. Вики для получения дополнительной информации.
Было бы здорово, если бы это было, но это не в настоящее время. Я написал статью об этом, но, в итоге, существует давнее языковое предложение, посвященное типам значений, не связанных с декоративными. Наличие необоснованных типов ценностей является отличным первым шагом, но также было бы удобно иметь что-то на языке для обеспечения проверки. Поэтому я добавил языковое предложение для инвариантных записей.
В одном из ответов в предложении говорится, что языковая группа решила, что политика валидации не должна быть частью C#, а предоставлена генераторами источников.
Сильнотиптид это сосредоточено больше на удостоверении личности. Vogen сосредоточен больше на «доменных концепциях» и ограничениях, связанных с этими понятиями.
Спригипед, это моя первая попытка и не сгенерирована источником. Есть накладные расходы на память, потому что базовый тип - это класс. Также нет анализаторов. Теперь он отмечается как устаревший в пользу Vogen.
Значение, похожее на Strighlytyped - не сгенерированный исходным и без анализаторов. Это также более расслабленное и позволяет композитным «лежащим» типам.
ValueObjectGenerator, аналогичный Vogen, но менее сфокусирован на проверке и отсутствии анализатора кода.
Любой тип может быть завернут. Сериализация и конверсии типа имеют реализации для:
нить
инт
длинный
короткий
байт
float (сингл)
Десятичный
двойной
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 .
TimeOnly говоря, что 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. Если кто -то генерирует .proto файлы для других приложений из C# кода, прото -файлы будут включать в себя тип суррогата в качестве типа. Спасибо @domasm за эту информацию .
Спасибо всем, кто внес свой вклад!
Я черпал вдохновение у Эндрю Локка Сильностидена.
Я также получил несколько отличных идей от Meziantou's Gérald Barré.analyzer