From 25cb85e09c3c0068534a7fb283897d3064d56f08 Mon Sep 17 00:00:00 2001 From: James Upjohn Date: Sun, 22 Jan 2023 11:18:41 +1300 Subject: [PATCH 1/4] fix: parse generic type when using generic base class --- src/Vogen/ManageAttributes.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Vogen/ManageAttributes.cs b/src/Vogen/ManageAttributes.cs index 02a3fb3a04..7ce345b279 100644 --- a/src/Vogen/ManageAttributes.cs +++ b/src/Vogen/ManageAttributes.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -68,7 +68,8 @@ public static VogenConfigurationBuildResult TryBuildConfigurationFromAttribute(A bool hasErroredAttributes = false; - if (!matchingAttribute.ConstructorArguments.IsEmpty) + var isBaseGenericType = matchingAttribute.AttributeClass!.BaseType!.IsGenericType; + if (!matchingAttribute.ConstructorArguments.IsEmpty || isBaseGenericType) { // make sure we don't have any errors ImmutableArray args = matchingAttribute.ConstructorArguments; @@ -82,7 +83,7 @@ public static VogenConfigurationBuildResult TryBuildConfigurationFromAttribute(A } // find which constructor to use, it could be the generic attribute (> C# 11), or the non-generic. - if (matchingAttribute.AttributeClass!.IsGenericType) + if (matchingAttribute.AttributeClass!.IsGenericType || isBaseGenericType) { PopulateFromGenericAttribute(matchingAttribute, args); } @@ -175,7 +176,14 @@ void PopulateFromGenericAttribute( AttributeData attributeData, ImmutableArray args) { - var type = attributeData.AttributeClass!.TypeArguments[0] as INamedTypeSymbol; + var isDerivedFromGenericAttribute = + attributeData.AttributeClass!.BaseType!.FullName()!.StartsWith("Vogen.ValueObjectAttribute<"); + + // Extracts the generic argument from the base type when the derived type isn't generic + // e.g. MyCustomVoAttribute : ValueObjectAttribute + var type = isDerivedFromGenericAttribute && attributeData.AttributeClass!.TypeArguments.IsEmpty + ? attributeData.AttributeClass!.BaseType!.TypeArguments[0] as INamedTypeSymbol + : attributeData.AttributeClass!.TypeArguments[0] as INamedTypeSymbol; switch (args.Length) { case 5: From 086d5de55f1acc8882814d237e2158c031fb86b6 Mon Sep 17 00:00:00 2001 From: James Upjohn Date: Sun, 22 Jan 2023 11:25:32 +1300 Subject: [PATCH 2/4] chore(test): set non-int generic type and conversions derived in derived attribute test --- tests/SnapshotTests/GenericAttributeTests.cs | 6 +- ...tances_with_derived_attribute.verified.txt | 86 +++++++++++-------- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/tests/SnapshotTests/GenericAttributeTests.cs b/tests/SnapshotTests/GenericAttributeTests.cs index accffdabfa..3eec149dba 100644 --- a/tests/SnapshotTests/GenericAttributeTests.cs +++ b/tests/SnapshotTests/GenericAttributeTests.cs @@ -64,7 +64,11 @@ public Task Produces_instances_with_derived_attribute() namespace Whatever; -public class CustomGenericAttribute : ValueObjectAttribute { +public class CustomGenericAttribute : ValueObjectAttribute +{ + public CustomGenericAttribute(Conversions conversions = Conversions.Default | Conversions.EfCoreValueConverter) + { + } } [CustomGenericAttribute] diff --git a/tests/SnapshotTests/snapshots/snap-v7.0/GenericAttributeTests.Produces_instances_with_derived_attribute.verified.txt b/tests/SnapshotTests/snapshots/snap-v7.0/GenericAttributeTests.Produces_instances_with_derived_attribute.verified.txt index 62a846810b..8f7e2cdd08 100644 --- a/tests/SnapshotTests/snapshots/snap-v7.0/GenericAttributeTests.Produces_instances_with_derived_attribute.verified.txt +++ b/tests/SnapshotTests/snapshots/snap-v7.0/GenericAttributeTests.Produces_instances_with_derived_attribute.verified.txt @@ -34,8 +34,8 @@ namespace Whatever [global::System.ComponentModel.TypeConverter(typeof(CustomerIdTypeConverter))] [global::System.Diagnostics.DebuggerTypeProxyAttribute(typeof(CustomerIdDebugView))] - [global::System.Diagnostics.DebuggerDisplayAttribute("Underlying type: System.Int32, Value = { _value }")] - public partial struct CustomerId : global::System.IEquatable, global::System.IEquatable , global::System.IComparable + [global::System.Diagnostics.DebuggerDisplayAttribute("Underlying type: System.Int64, Value = { _value }")] + public partial struct CustomerId : global::System.IEquatable, global::System.IEquatable , global::System.IComparable { #if DEBUG private readonly global::System.Diagnostics.StackTrace _stackTrace = null; @@ -43,12 +43,12 @@ namespace Whatever private readonly global::System.Boolean _isInitialized; - private readonly System.Int32 _value; + private readonly System.Int64 _value; /// - /// Gets the underlying value if set, otherwise a is thrown. + /// Gets the underlying value if set, otherwise a is thrown. /// - public readonly System.Int32 Value + public readonly System.Int64 Value { [global::System.Diagnostics.DebuggerStepThroughAttribute] get @@ -71,7 +71,7 @@ namespace Whatever } [global::System.Diagnostics.DebuggerStepThroughAttribute] - private CustomerId(System.Int32 value) + private CustomerId(System.Int64 value) { _value = value; _isInitialized = true; @@ -82,7 +82,7 @@ namespace Whatever /// /// The underlying type. /// An instance of this type. - public static CustomerId From(System.Int32 value) + public static CustomerId From(System.Int64 value) { @@ -93,12 +93,12 @@ namespace Whatever return instance; } - public static explicit operator CustomerId(System.Int32 value) => From(value); - public static explicit operator System.Int32(CustomerId value) => value.Value; + public static explicit operator CustomerId(System.Int64 value) => From(value); + public static explicit operator System.Int64(CustomerId value) => value.Value; // only called internally when something has been deserialized into // its primitive type. - private static CustomerId Deserialize(System.Int32 value) + private static CustomerId Deserialize(System.Int64 value) { @@ -118,10 +118,10 @@ namespace Whatever // We treat anything uninitialized as not equal to anything, even other uninitialized instances of this type. if(!_isInitialized || !other._isInitialized) return false; - return global::System.Collections.Generic.EqualityComparer.Default.Equals(Value, other.Value); + return global::System.Collections.Generic.EqualityComparer.Default.Equals(Value, other.Value); } - public readonly global::System.Boolean Equals(System.Int32 primitive) => Value.Equals(primitive); + public readonly global::System.Boolean Equals(System.Int64 primitive) => Value.Equals(primitive); public readonly override global::System.Boolean Equals(global::System.Object obj) { @@ -131,16 +131,16 @@ namespace Whatever public static global::System.Boolean operator ==(CustomerId left, CustomerId right) => Equals(left, right); public static global::System.Boolean operator !=(CustomerId left, CustomerId right) => !(left == right); - public static global::System.Boolean operator ==(CustomerId left, System.Int32 right) => Equals(left.Value, right); - public static global::System.Boolean operator !=(CustomerId left, System.Int32 right) => !Equals(left.Value, right); + public static global::System.Boolean operator ==(CustomerId left, System.Int64 right) => Equals(left.Value, right); + public static global::System.Boolean operator !=(CustomerId left, System.Int64 right) => !Equals(left.Value, right); - public static global::System.Boolean operator ==(System.Int32 left, CustomerId right) => Equals(left, right.Value); - public static global::System.Boolean operator !=(System.Int32 left, CustomerId right) => !Equals(left, right.Value); + public static global::System.Boolean operator ==(System.Int64 left, CustomerId right) => Equals(left, right.Value); + public static global::System.Boolean operator !=(System.Int64 left, CustomerId right) => !Equals(left, right.Value); public int CompareTo(CustomerId other) => Value.CompareTo(other.Value); - /// + /// /// /// /// @@ -152,7 +152,7 @@ namespace Whatever [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] #endif out CustomerId result) { - if(System.Int32.TryParse(s, style, provider, out var r)) { + if(System.Int64.TryParse(s, style, provider, out var r)) { result = From(r); return true; } @@ -161,7 +161,7 @@ namespace Whatever return false; } - /// + /// /// /// /// @@ -173,7 +173,7 @@ namespace Whatever [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] #endif out CustomerId result) { - if(System.Int32.TryParse(s, provider, out var r)) { + if(System.Int64.TryParse(s, provider, out var r)) { result = From(r); return true; } @@ -182,7 +182,7 @@ namespace Whatever return false; } - /// + /// /// /// /// @@ -194,7 +194,7 @@ namespace Whatever [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] #endif out CustomerId result) { - if(System.Int32.TryParse(s, out var r)) { + if(System.Int64.TryParse(s, out var r)) { result = From(r); return true; } @@ -203,7 +203,7 @@ namespace Whatever return false; } - /// + /// /// /// /// @@ -215,7 +215,7 @@ namespace Whatever [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] #endif out CustomerId result) { - if(System.Int32.TryParse(s, style, provider, out var r)) { + if(System.Int64.TryParse(s, style, provider, out var r)) { result = From(r); return true; } @@ -224,7 +224,7 @@ namespace Whatever return false; } - /// + /// /// /// /// @@ -236,7 +236,7 @@ namespace Whatever [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] #endif out CustomerId result) { - if(System.Int32.TryParse(s, provider, out var r)) { + if(System.Int64.TryParse(s, provider, out var r)) { result = From(r); return true; } @@ -245,7 +245,7 @@ namespace Whatever return false; } - /// + /// /// /// /// @@ -257,7 +257,7 @@ namespace Whatever [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] #endif out CustomerId result) { - if(System.Int32.TryParse(s, out var r)) { + if(System.Int64.TryParse(s, out var r)) { result = From(r); return true; } @@ -267,10 +267,10 @@ namespace Whatever } - public readonly override global::System.Int32 GetHashCode() => global::System.Collections.Generic.EqualityComparer.Default.GetHashCode(_value); + public readonly override global::System.Int32 GetHashCode() => global::System.Collections.Generic.EqualityComparer.Default.GetHashCode(_value); /// Returns the string representation of the underlying type - /// + /// public readonly override global::System.String ToString() => Value.ToString(); private readonly void EnsureInitialized() @@ -320,7 +320,7 @@ public static readonly CustomerId Cust42 = new CustomerId(42); { public override CustomerId Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) { - return CustomerId.Deserialize(reader.GetInt32()); + return CustomerId.Deserialize(reader.GetInt64()); } public override void Write(System.Text.Json.Utf8JsonWriter writer, CustomerId value, global::System.Text.Json.JsonSerializerOptions options) @@ -334,29 +334,29 @@ public static readonly CustomerId Cust42 = new CustomerId(42); { public override global::System.Boolean CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) { - return sourceType == typeof(global::System.Int32) || sourceType == typeof(global::System.String) || base.CanConvertFrom(context, sourceType); + return sourceType == typeof(global::System.Int64) || sourceType == typeof(global::System.String) || base.CanConvertFrom(context, sourceType); } public override global::System.Object ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Globalization.CultureInfo culture, global::System.Object value) { return value switch { - global::System.Int32 intValue => CustomerId.Deserialize(intValue), - global::System.String stringValue when !global::System.String.IsNullOrEmpty(stringValue) && global::System.Int32.TryParse(stringValue, out var result) => CustomerId.Deserialize(result), + global::System.Int64 longValue => CustomerId.Deserialize(longValue), + global::System.String stringValue when !global::System.String.IsNullOrEmpty(stringValue) && long.TryParse(stringValue, out var result) => CustomerId.Deserialize(result), _ => base.ConvertFrom(context, culture, value), }; } public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) { - return sourceType == typeof(global::System.Int32) || sourceType == typeof(global::System.String) || base.CanConvertTo(context, sourceType); + return sourceType == typeof(global::System.Int64) || sourceType == typeof(global::System.String) || base.CanConvertTo(context, sourceType); } public override object ConvertTo(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Globalization.CultureInfo culture, global::System.Object value, global::System.Type destinationType) { if (value is CustomerId idValue) { - if (destinationType == typeof(global::System.Int32)) + if (destinationType == typeof(global::System.Int64)) { return idValue.Value; } @@ -372,6 +372,16 @@ public static readonly CustomerId Cust42 = new CustomerId(42); } + public class EfCoreValueConverter : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter + { + public EfCoreValueConverter() : this(null) { } + public EfCoreValueConverter(global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ConverterMappingHints mappingHints = null) + : base( + vo => vo.Value, + value => CustomerId.Deserialize(value), + mappingHints + ) { } + } @@ -385,14 +395,14 @@ public static readonly CustomerId Cust42 = new CustomerId(42); } public global::System.Boolean IsInitialized => _t._isInitialized; - public global::System.String UnderlyingType => "System.Int32"; + public global::System.String UnderlyingType => "System.Int64"; public global::System.String Value => _t._isInitialized ? _t._value.ToString() : "[not initialized]" ; #if DEBUG public global::System.String CreatedWith => _t._stackTrace?.ToString() ?? "the From method"; #endif - public global::System.String Conversions => @"Default"; + public global::System.String Conversions => @"Default, EfCoreValueConverter"; } } From aa1fde1a130dc701c4d647010cc73654ea091048 Mon Sep 17 00:00:00 2001 From: James Upjohn Date: Sun, 22 Jan 2023 11:49:42 +1300 Subject: [PATCH 3/4] chore(doc): add section explaining usage of custom VO attributes --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index f6d9b363b5..b46fed8e5f 100644 --- a/README.md +++ b/README.md @@ -597,6 +597,20 @@ public void CanEnter(Age age) { } ``` +### Can I create custom value object attributes with my own defaults? + +Yes, but (at the moment) it requires that you put your defaults in your attribute's primary constructor - not in the call to the base class' constructor (see [this comment](https://github.com/SteveDunn/Vogen/pull/321#issuecomment-1399324832)). + +```csharp +public class CustomValueObjectAttribute : ValueObjectAttribute +{ + // This attribute will default to having both the default conversions and EF Core type conversions + public CustomValueObjectAttribute(Conversions conversions = Conversions.Default | Conversions.EfCoreValueConverter) { } +} +``` + +NOTE: *custom attributes must extend a ValueObjectAttribute class; you cannot layer custom attributes on top of each other* + ### Why isn't this concept part of the C# language? It would be great if it was, but it's not currently. I [wrote an article about it](https://dunnhq.com/posts/2022/non-defaultable-value-types/), but in summary, there is a [long-standing language proposal](https://github.com/dotnet/csharplang/issues/146) focusing on non-defaultable value types. From 8f73c486e8bb3d2cd9b2a933c55546be947981ae Mon Sep 17 00:00:00 2001 From: James Upjohn Date: Sun, 22 Jan 2023 20:57:45 +1300 Subject: [PATCH 4/4] chore(doc); remove reference to primary constructor for non-primary ctor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b46fed8e5f..ff49f09af5 100644 --- a/README.md +++ b/README.md @@ -599,7 +599,7 @@ public void CanEnter(Age age) { ### Can I create custom value object attributes with my own defaults? -Yes, but (at the moment) it requires that you put your defaults in your attribute's primary constructor - not in the call to the base class' constructor (see [this comment](https://github.com/SteveDunn/Vogen/pull/321#issuecomment-1399324832)). +Yes, but (at the moment) it requires that you put your defaults in your attribute's constructor - not in the call to the base class' constructor (see [this comment](https://github.com/SteveDunn/Vogen/pull/321#issuecomment-1399324832)). ```csharp public class CustomValueObjectAttribute : ValueObjectAttribute