Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: extract constructor parameters & generic type info form derived attributes #340

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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<long>
{
// 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.
Expand Down
16 changes: 12 additions & 4 deletions src/Vogen/ManageAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
Expand Down Expand Up @@ -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<TypedConstant> args = matchingAttribute.ConstructorArguments;
Expand All @@ -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);
}
Expand Down Expand Up @@ -175,7 +176,14 @@ void PopulateFromGenericAttribute(
AttributeData attributeData,
ImmutableArray<TypedConstant> 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<long>
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:
Expand Down
6 changes: 5 additions & 1 deletion tests/SnapshotTests/GenericAttributeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ public Task Produces_instances_with_derived_attribute()

namespace Whatever;

public class CustomGenericAttribute : ValueObjectAttribute<int> {
public class CustomGenericAttribute : ValueObjectAttribute<long>
{
public CustomGenericAttribute(Conversions conversions = Conversions.Default | Conversions.EfCoreValueConverter)
{
}
}

[CustomGenericAttribute]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,21 @@ 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<CustomerId>, global::System.IEquatable<System.Int32> , global::System.IComparable<CustomerId>
[global::System.Diagnostics.DebuggerDisplayAttribute("Underlying type: System.Int64, Value = { _value }")]
public partial struct CustomerId : global::System.IEquatable<CustomerId>, global::System.IEquatable<System.Int64> , global::System.IComparable<CustomerId>
{
#if DEBUG
private readonly global::System.Diagnostics.StackTrace _stackTrace = null;
#endif

private readonly global::System.Boolean _isInitialized;

private readonly System.Int32 _value;
private readonly System.Int64 _value;

/// <summary>
/// Gets the underlying <see cref="System.Int32" /> value if set, otherwise a <see cref="ValueObjectValidationException" /> is thrown.
/// Gets the underlying <see cref="System.Int64" /> value if set, otherwise a <see cref="ValueObjectValidationException" /> is thrown.
/// </summary>
public readonly System.Int32 Value
public readonly System.Int64 Value
{
[global::System.Diagnostics.DebuggerStepThroughAttribute]
get
Expand All @@ -71,7 +71,7 @@ namespace Whatever
}

[global::System.Diagnostics.DebuggerStepThroughAttribute]
private CustomerId(System.Int32 value)
private CustomerId(System.Int64 value)
{
_value = value;
_isInitialized = true;
Expand All @@ -82,7 +82,7 @@ namespace Whatever
/// </summary>
/// <param name="value">The underlying type.</param>
/// <returns>An instance of this type.</returns>
public static CustomerId From(System.Int32 value)
public static CustomerId From(System.Int64 value)
{


Expand All @@ -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)
{


Expand All @@ -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<System.Int32>.Default.Equals(Value, other.Value);
return global::System.Collections.Generic.EqualityComparer<System.Int64>.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)
{
Expand All @@ -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);


/// <inheritdoc cref="int.TryParse(System.ReadOnlySpan{char}, System.Globalization.NumberStyles, System.IFormatProvider?, out int)"/>
/// <inheritdoc cref="long.TryParse(System.ReadOnlySpan{char}, System.Globalization.NumberStyles, System.IFormatProvider?, out long)"/>
/// <summary>
/// </summary>
/// <returns>
Expand All @@ -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;
}
Expand All @@ -161,7 +161,7 @@ namespace Whatever
return false;
}

/// <inheritdoc cref="int.TryParse(System.ReadOnlySpan{char}, System.IFormatProvider?, out int)"/>
/// <inheritdoc cref="long.TryParse(System.ReadOnlySpan{char}, System.IFormatProvider?, out long)"/>
/// <summary>
/// </summary>
/// <returns>
Expand All @@ -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;
}
Expand All @@ -182,7 +182,7 @@ namespace Whatever
return false;
}

/// <inheritdoc cref="int.TryParse(System.ReadOnlySpan{char}, out int)"/>
/// <inheritdoc cref="long.TryParse(System.ReadOnlySpan{char}, out long)"/>
/// <summary>
/// </summary>
/// <returns>
Expand All @@ -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;
}
Expand All @@ -203,7 +203,7 @@ namespace Whatever
return false;
}

/// <inheritdoc cref="int.TryParse(string?, System.Globalization.NumberStyles, System.IFormatProvider?, out int)"/>
/// <inheritdoc cref="long.TryParse(string?, System.Globalization.NumberStyles, System.IFormatProvider?, out long)"/>
/// <summary>
/// </summary>
/// <returns>
Expand All @@ -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;
}
Expand All @@ -224,7 +224,7 @@ namespace Whatever
return false;
}

/// <inheritdoc cref="int.TryParse(string?, System.IFormatProvider?, out int)"/>
/// <inheritdoc cref="long.TryParse(string?, System.IFormatProvider?, out long)"/>
/// <summary>
/// </summary>
/// <returns>
Expand All @@ -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;
}
Expand All @@ -245,7 +245,7 @@ namespace Whatever
return false;
}

/// <inheritdoc cref="int.TryParse(string?, out int)"/>
/// <inheritdoc cref="long.TryParse(string?, out long)"/>
/// <summary>
/// </summary>
/// <returns>
Expand All @@ -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;
}
Expand All @@ -267,10 +267,10 @@ namespace Whatever
}


public readonly override global::System.Int32 GetHashCode() => global::System.Collections.Generic.EqualityComparer<System.Int32>.Default.GetHashCode(_value);
public readonly override global::System.Int32 GetHashCode() => global::System.Collections.Generic.EqualityComparer<System.Int64>.Default.GetHashCode(_value);

/// <summary>Returns the string representation of the underlying type</summary>
/// <inheritdoc cref="System.Int32.ToString()" />
/// <inheritdoc cref="System.Int64.ToString()" />
public readonly override global::System.String ToString() => Value.ToString();

private readonly void EnsureInitialized()
Expand Down Expand Up @@ -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)
Expand All @@ -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;
}
Expand All @@ -372,6 +372,16 @@ public static readonly CustomerId Cust42 = new CustomerId(42);
}


public class EfCoreValueConverter : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<CustomerId, global::System.Int64>
{
public EfCoreValueConverter() : this(null) { }
public EfCoreValueConverter(global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ConverterMappingHints mappingHints = null)
: base(
vo => vo.Value,
value => CustomerId.Deserialize(value),
mappingHints
) { }
}



Expand All @@ -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";
}

}
Expand Down