From 78c7ea6311c33100cb93f3699de160f0a431fce2 Mon Sep 17 00:00:00 2001 From: Sir Phillip Tubell Date: Wed, 17 Jun 2026 16:09:12 -0400 Subject: [PATCH 1/5] feat: add stringDefaultComparison for type-level default string comparer Adds a new `StringComparisonDefault` enum and `stringDefaultComparison` parameter to `ValueObjectAttribute`, `ValueObjectAttribute`, and `VogenDefaultsAttribute`. When set, the specified `StringComparer` iswired into all five equality/hashing entry points on string-backed value objects: `Equals(T)`, `Equals(string)`, `operator ==` (both directions), and `GetHashCode()`. The `Comparers` nested class is generated implicitly when a default comparison is specified. Two new diagnostics: - VOG039 (warning): `stringDefaultComparison` set on a non-string type - VOG040 (error): `stringComparers: Omit` explicitly conflicts with `stringDefaultComparison` Also fixes a bug in the existing `Comparers` class where `GetHashCode` called `_comparer.GetHashCode()` (the comparer's own object hash) instead of `_comparer.GetHashCode(obj._value)`, causing all values to hash identically and degrading dictionary lookups to O(n). --- .../StringComparisonDefault.cs | 17 + src/Vogen.SharedTypes/ValueObjectAttribute.cs | 134 +++- .../VogenDefaultsAttribute.cs | 65 ++ src/Vogen/BuildConfigurationFromAttributes.cs | 27 +- src/Vogen/BuildWorkItems.cs | 8 + src/Vogen/CombineConfigurations.cs | 11 +- src/Vogen/Diagnostics/DiagnosticsCatalogue.cs | 17 + src/Vogen/Diagnostics/RuleIdentifiers.cs | 2 + ...enerateCodeForEqualsMethodsAndOperators.cs | 36 +- src/Vogen/GenerateCodeForHashCodes.cs | 20 +- src/Vogen/GenerateCodeForStringComparers.cs | 9 +- src/Vogen/VogenConfiguration.cs | 17 +- .../StringComparisons/ForClasses.cs | 18 + .../StringComparisons/ForRecordClasses.cs | 18 + .../StringComparisons/ForRecordStructs.cs | 18 + .../StringComparisons/ForStructs.cs | 18 + .../ConsumerTests/StringComparisons/Types.cs | 20 + .../StringComparisonGenerationTests.cs | 17 + ...ests.Generates_when_specified.verified.txt | 2 +- ..._comparison_OrdinalIgnoreCase.verified.txt | 620 ++++++++++++++++++ .../VogenConfigurationTests.cs | 9 +- 21 files changed, 1073 insertions(+), 30 deletions(-) create mode 100644 src/Vogen.SharedTypes/StringComparisonDefault.cs create mode 100644 tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt diff --git a/src/Vogen.SharedTypes/StringComparisonDefault.cs b/src/Vogen.SharedTypes/StringComparisonDefault.cs new file mode 100644 index 00000000000..ea72024f1c8 --- /dev/null +++ b/src/Vogen.SharedTypes/StringComparisonDefault.cs @@ -0,0 +1,17 @@ +using System.Diagnostics; +// ReSharper disable UnusedMember.Global + +namespace Vogen; + +public enum StringComparisonDefault +{ + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + Unspecified = -1, + Omit = 0, + Ordinal = 1, + OrdinalIgnoreCase = 2, + CurrentCulture = 3, + CurrentCultureIgnoreCase = 4, + InvariantCulture = 5, + InvariantCultureIgnoreCase = 6, +} diff --git a/src/Vogen.SharedTypes/ValueObjectAttribute.cs b/src/Vogen.SharedTypes/ValueObjectAttribute.cs index 419f8d9b7a8..1d89feb5c44 100644 --- a/src/Vogen.SharedTypes/ValueObjectAttribute.cs +++ b/src/Vogen.SharedTypes/ValueObjectAttribute.cs @@ -1,4 +1,4 @@ -// ReSharper disable UnusedParameter.Local +// ReSharper disable UnusedParameter.Local // ReSharper disable NullableWarningSuppressionIsUsed // ReSharper disable UnusedType.Global @@ -88,6 +88,78 @@ public ValueObjectAttribute( numericsGeneration) { } + + /// + /// Configures aspects of this individual value object. + /// + /// Specifies what conversion code is generated - defaults to which generates type converters and a converter to handle serialization using System.Text.Json + /// Specifies the type of exception thrown when validation fails—defaults to . + /// Simple customization switches—defaults to . + /// Specifies how strict deserialization is, e.g. should your Validate method be called, or should pre-defined instances that otherwise invalid be allowed - defaults to . + /// Specifies the level that debug attributes are written as some IDEs don't support all of them, e.g., Rider - defaults to which generates DebuggerDisplay and a debugger proxy type for IDEs that support them + /// Species which comparison code is generated—defaults to which hoists any IComparable implementations from the primitive. + /// Specifies which string comparison code is generated—defaults to which doesn't generate anything related to string comparison. + /// Controls how cast operators are generated for casting from the Value Object to the primitive. + /// Options are implicit or explicit or none. Explicit is preferred over implicit if you really need them, but isn't recommended. + /// See for more information. + /// Controls how cast operators are generated for casting from the primitive to the Value Object. + /// Options are implicit or explicit or none. Explicit is preferred over implicit if you really need them, but isn't recommended. + /// See for more information. + /// + /// Specifies what is generated for IParsable types for strings - defaults to . + /// Specifies what is generated for Parse and TryParse methods - defaults to . + /// Specifies what to write for TryFrom methods—defaults to . + /// Specifies whether to generate an IsInitialized() method - defaults to . + /// + /// Specifies whether to generate primitive comparison operators, allowing this type to be compared for equality to the primitive. + /// Defaults to + /// + /// + /// var vo = MyInt.From(123); + /// + /// + /// bool same = vo == 123; + /// + /// + /// + /// Specifies whether to generate numeric interfaces (INumber<T> or INumberBase<T> depending on the underlying type)—defaults to . + /// Specifies the default used for ==, Equals, and GetHashCode on string-backed value objects—defaults to which uses the underlying string's default comparison. + public ValueObjectAttribute( + Conversions conversions = Conversions.Unspecified, + Type? throws = null!, + Customizations customizations = Customizations.None, + DeserializationStrictness deserializationStrictness = DeserializationStrictness.AllowValidAndKnownInstances, + DebuggerAttributeGeneration debuggerAttributes = DebuggerAttributeGeneration.Default, + ComparisonGeneration comparison = ComparisonGeneration.Default, + StringComparersGeneration stringComparers = StringComparersGeneration.Unspecified, + CastOperator toPrimitiveCasting = CastOperator.Unspecified, + CastOperator fromPrimitiveCasting = CastOperator.Unspecified, + ParsableForStrings parsableForStrings = ParsableForStrings.Unspecified, + ParsableForPrimitives parsableForPrimitives = ParsableForPrimitives.Unspecified, + TryFromGeneration tryFromGeneration = TryFromGeneration.Unspecified, + IsInitializedMethodGeneration isInitializedMethodGeneration = IsInitializedMethodGeneration.Unspecified, + PrimitiveEqualityGeneration primitiveEqualityGeneration = PrimitiveEqualityGeneration.Unspecified, + NumericsGeneration numericsGeneration = NumericsGeneration.Unspecified, + StringComparisonDefault stringDefaultComparison = StringComparisonDefault.Unspecified) : base( + typeof(T), + conversions, + throws, + customizations, + deserializationStrictness, + debuggerAttributes, + comparison, + stringComparers, + toPrimitiveCasting, + fromPrimitiveCasting, + parsableForStrings, + parsableForPrimitives, + tryFromGeneration, + isInitializedMethodGeneration, + primitiveEqualityGeneration, + numericsGeneration, + stringDefaultComparison) + { + } } /// @@ -100,7 +172,7 @@ public class ValueObjectAttribute : Attribute // keep this signature in-line with `VogenConfiguration` // as the syntax/semantics are read in the generator // using parameter indexes (i.e. it expected param 0 to be the underlying type etc). - + // ReSharper disable once MemberCanBeProtected.Global /// @@ -156,5 +228,61 @@ public ValueObjectAttribute( // of parameters is a binary-breaking change. See https://github.com/dotnet/runtime/issues/103722 // for more information. } + + /// + /// Configures aspects of this individual value object. + /// + /// The type of the primitive that is being wrapped—defaults to int. + /// Specifies what conversion code is generated - defaults to which generates type converters and a converter to handle serialization using System.Text.Json + /// Specifies the type of exception thrown when validation fails—defaults to . + /// Simple customization switches—defaults to . + /// Specifies how strict deserialization is, e.g. should your Validate method be called, or should pre-defined instances that otherwise invalid be allowed - defaults to . + /// Specifies the level that debug attributes are written as some IDEs don't support all of them, e.g., Rider - defaults to which generates DebuggerDisplay and a debugger proxy type for IDEs that support them + /// Species which comparison code is generated—defaults to which hoists any IComparable implementations from the primitive. + /// Specifies which string comparison code is generated—defaults to which doesn't generate anything related to string comparison. + /// Specifies the type of casting from wrapper to primitive - defaults to . + /// Specifies the type of casting from primitive to wrapper - default to . + /// Specifies what is generated for IParsable types for strings - defaults to . + /// Specifies what is generated for Parse and TryParse methods - defaults to . + /// Specifies what to write for TryFrom methods—defaults to . + /// Specifies whether to generate an IsInitialized() method - defaults to . + /// + /// Specifies whether to generate primitive comparison operators, allowing this type to be compared for equality to the primitive. + /// Defaults to + /// + /// + /// var vo = MyInt.From(123); + /// + /// + /// bool same = vo == 123; + /// + /// + /// + /// Specifies whether to generate numeric interfaces (INumber<T> or INumberBase<T> depending on the underlying type)—defaults to . + /// Specifies the default used for ==, Equals, and GetHashCode on string-backed value objects—defaults to which uses the underlying string's default comparison. + public ValueObjectAttribute( + Type? underlyingType = null!, + Conversions conversions = Conversions.Unspecified, + Type? throws = null!, + Customizations customizations = Customizations.None, + DeserializationStrictness deserializationStrictness = DeserializationStrictness.AllowValidAndKnownInstances, + DebuggerAttributeGeneration debuggerAttributes = DebuggerAttributeGeneration.Default, + ComparisonGeneration comparison = ComparisonGeneration.Default, + StringComparersGeneration stringComparers = StringComparersGeneration.Unspecified, + CastOperator toPrimitiveCasting = CastOperator.Unspecified, + CastOperator fromPrimitiveCasting = CastOperator.Unspecified, + ParsableForStrings parsableForStrings = ParsableForStrings.Unspecified, + ParsableForPrimitives parsableForPrimitives = ParsableForPrimitives.Unspecified, + TryFromGeneration tryFromGeneration = TryFromGeneration.Unspecified, + IsInitializedMethodGeneration isInitializedMethodGeneration = IsInitializedMethodGeneration.Unspecified, + PrimitiveEqualityGeneration primitiveEqualityGeneration = PrimitiveEqualityGeneration.Unspecified, + NumericsGeneration numericsGeneration = NumericsGeneration.Unspecified, + StringComparisonDefault stringDefaultComparison = StringComparisonDefault.Unspecified) + { + // DO NOT ADD PARAMETERS HERE, INSTEAD, CREATE OVERLOADS (at least until a new major version). + // This is because some users use reflection to find this attribute, and changing the amount + // of parameters is a binary-breaking change. See https://github.com/dotnet/runtime/issues/103722 + // for more information. + } } -} \ No newline at end of file +} diff --git a/src/Vogen.SharedTypes/VogenDefaultsAttribute.cs b/src/Vogen.SharedTypes/VogenDefaultsAttribute.cs index 1990470c41d..90cef74d8b0 100644 --- a/src/Vogen.SharedTypes/VogenDefaultsAttribute.cs +++ b/src/Vogen.SharedTypes/VogenDefaultsAttribute.cs @@ -76,4 +76,69 @@ public VogenDefaultsAttribute( NumericsGeneration numericsGeneration = NumericsGeneration.Unspecified) { } + + /// + /// Creates a new instance of a type that represents the default + /// values used for value object generation. + /// + /// The type of the primitive that is being wrapped—defaults to int. + /// Specifies what conversion code is generated - defaults to which generates type converters and a converter to handle serialization using System.Text.Json + /// Specifies the type of exception thrown when validation fails—defaults to . + /// Simple customization switches—defaults to . + /// Specifies how strict deserialization is, e.g. should your Validate method be called, or should pre-defined instances that otherwise invalid be allowed - defaults to . + /// Specifies the level that debug attributes are written as some IDEs don't support all of them, e.g., Rider - defaults to which generates DebuggerDisplay and a debugger proxy type for IDEs that support them + /// Controls how cast operators are generated for casting from the Value Object to the primitive. + /// Options are implicit or explicit or none. Explicit is preferred over implicit if you really need them, but isn't recommended. + /// See for more information. + /// Controls how cast operators are generated for casting from the primitive to the Value Object. + /// Options are implicit or explicit or none. Explicit is preferred over implicit if you really need them, but isn't recommended. + /// See <see href="https://github.com/SteveDunn/Vogen/wiki/Casting"/> for more information. + /// disables stack trace recording; in Debug builds, a stack trace is recorded and is + /// thrown in the exception when something is created in an uninitialized state, e.g. after deserialization + /// Specifies what is generated for IParsable types for strings - defaults to . + /// Specifies what is generated for Parse and TryParse methods - defaults to . + /// Specifies what to write for TryFrom methods—defaults to . + /// Specifies whether to generate an IsInitialized() method - defaults to . + /// + /// Specifies whether to generate primitive comparison operators, allowing this type to be compared for equality to the primitive. + /// Defaults to + /// + /// + /// var vo = MyInt.From(123); + /// + /// + /// bool same = vo == 123; + /// + /// + /// + /// Controls the generation of the type factory for System.Text.Json. + /// Controls the generation of static abstract interfaces. + /// Controls the generation of a Swashbuckle schema filter for OpenAPI. + /// Every ValueObject attribute must explicitly specify the type of the primitive. + /// Specifies whether to generate numeric interfaces (INumber<T> or INumberBase<T> depending on the underlying type)—defaults to . + /// Specifies the default used for ==, Equals, and GetHashCode on string-backed value objects—defaults to which uses the underlying string's default comparison. + public VogenDefaultsAttribute( + Type? underlyingType = null, + Conversions conversions = Conversions.Unspecified, + Type? throws = null, + Customizations customizations = Customizations.None, + DeserializationStrictness deserializationStrictness = DeserializationStrictness.AllowValidAndKnownInstances, + DebuggerAttributeGeneration debuggerAttributes = DebuggerAttributeGeneration.Default, + CastOperator toPrimitiveCasting = CastOperator.Explicit, + CastOperator fromPrimitiveCasting = CastOperator.Explicit, + bool disableStackTraceRecordingInDebug = false, + ParsableForStrings parsableForStrings = ParsableForStrings.GenerateMethodsAndInterface, + ParsableForPrimitives parsableForPrimitives = ParsableForPrimitives.HoistMethodsAndInterfaces, + TryFromGeneration tryFromGeneration = TryFromGeneration.Unspecified, + IsInitializedMethodGeneration isInitializedMethodGeneration = IsInitializedMethodGeneration.Unspecified, + SystemTextJsonConverterFactoryGeneration systemTextJsonConverterFactoryGeneration = + SystemTextJsonConverterFactoryGeneration.Unspecified, + StaticAbstractsGeneration staticAbstractsGeneration = StaticAbstractsGeneration.Unspecified, + OpenApiSchemaCustomizations openApiSchemaCustomizations = OpenApiSchemaCustomizations.Unspecified, + bool explicitlySpecifyTypeInValueObject = false, + PrimitiveEqualityGeneration primitiveEqualityGeneration = PrimitiveEqualityGeneration.Unspecified, + NumericsGeneration numericsGeneration = NumericsGeneration.Unspecified, + StringComparisonDefault stringDefaultComparison = StringComparisonDefault.Unspecified) + { + } } \ No newline at end of file diff --git a/src/Vogen/BuildConfigurationFromAttributes.cs b/src/Vogen/BuildConfigurationFromAttributes.cs index aadea943f34..0b1765d01d1 100644 --- a/src/Vogen/BuildConfigurationFromAttributes.cs +++ b/src/Vogen/BuildConfigurationFromAttributes.cs @@ -38,6 +38,7 @@ internal class BuildConfigurationFromAttributes private bool _primitiveTypeMustBeExplicit; private PrimitiveEqualityGeneration _primitiveEqualityGeneration; private NumericsGeneration _numericsGeneration; + private StringComparisonDefault _stringDefaultComparison; private BuildConfigurationFromAttributes(AttributeData att) { @@ -64,6 +65,7 @@ private BuildConfigurationFromAttributes(AttributeData att) _primitiveTypeMustBeExplicit = false; _primitiveEqualityGeneration = PrimitiveEqualityGeneration.Unspecified; _numericsGeneration = NumericsGeneration.Unspecified; + _stringDefaultComparison = StringComparisonDefault.Unspecified; _diagnostics = new List(); @@ -127,7 +129,8 @@ private VogenConfigurationBuildResult Build(bool argsAreFromVogenDefaultAttribut OpenApiSchemaCustomizations: _openApiSchemaCustomizations, ExplicitlySpecifyTypeInValueObject: _primitiveTypeMustBeExplicit, PrimitiveEqualityGeneration: _primitiveEqualityGeneration, - NumericsGeneration: _numericsGeneration), + NumericsGeneration: _numericsGeneration, + StringDefaultComparison: _stringDefaultComparison), diagnostics: _diagnostics); } @@ -194,6 +197,14 @@ private void PopulateDiagnosticsWithAnyValidationIssues() { _diagnostics.Add(DiagnosticsCatalogue.InvalidDeserializationStrictness(syntaxLocation)); } + + bool hasActiveDefaultComparison = _stringDefaultComparison is not StringComparisonDefault.Unspecified + and not StringComparisonDefault.Omit; + + if (hasActiveDefaultComparison && _stringComparers == StringComparersGeneration.Omit) + { + _diagnostics.Add(DiagnosticsCatalogue.StringDefaultComparisonConflictsWithStringComparersOmit(syntaxLocation)); + } } // populates all args - it doesn't expect the underlying type argument as that is: @@ -202,7 +213,7 @@ private void PopulateDiagnosticsWithAnyValidationIssues() // ReSharper disable once CognitiveComplexity private void PopulateFromVogenDefaultsAttributeArgs(ImmutableArray argsExcludingUnderlyingType) { - if (argsExcludingUnderlyingType.Length > 18) + if (argsExcludingUnderlyingType.Length > 19) { throw new InvalidOperationException("Too many arguments for the attribute."); } @@ -216,6 +227,11 @@ private void PopulateFromVogenDefaultsAttributeArgs(ImmutableArray args) { - if (args.Length > 15) + if (args.Length > 16) { throw new InvalidOperationException("Too many arguments for the attribute."); } @@ -327,6 +343,11 @@ private void PopulateFromValueObjectAttributeArgs(ImmutableArray continue; } + if (i == 15) + { + _stringDefaultComparison = (StringComparisonDefault) v; + } + if (i == 14) { _numericsGeneration = (NumericsGeneration) v; diff --git a/src/Vogen/BuildWorkItems.cs b/src/Vogen/BuildWorkItems.cs index 36a5a59c0a1..e719a8b4678 100644 --- a/src/Vogen/BuildWorkItems.cs +++ b/src/Vogen/BuildWorkItems.cs @@ -99,6 +99,14 @@ internal static class BuildWorkItems } } + bool hasActiveDefaultComparison = config.StringDefaultComparison is not StringComparisonDefault.Unspecified + and not StringComparisonDefault.Omit; + + if (hasActiveDefaultComparison && underlyingType.SpecialType != Microsoft.CodeAnalysis.SpecialType.System_String) + { + context.ReportDiagnostic(DiagnosticsCatalogue.StringDefaultComparisonNotApplicable(voSymbolInformation, underlyingType)); + } + IEnumerable instanceProperties = TryBuildInstanceProperties(allAttributes, voSymbolInformation, context, underlyingType, vogenKnownSymbols).ToList(); diff --git a/src/Vogen/CombineConfigurations.cs b/src/Vogen/CombineConfigurations.cs index 4f882cc9699..6f30fbca5b6 100644 --- a/src/Vogen/CombineConfigurations.cs +++ b/src/Vogen/CombineConfigurations.cs @@ -158,6 +158,14 @@ public static VogenConfiguration CombineAndResolveAnythingUnspecified( (var local, _) => local, }; + StringComparisonDefault stringDefaultComparison = (localValues.StringDefaultComparison, globalValues?.StringDefaultComparison) switch + { + (StringComparisonDefault.Unspecified, null) => VogenConfiguration.DefaultInstance.StringDefaultComparison, + (StringComparisonDefault.Unspecified, StringComparisonDefault.Unspecified) => VogenConfiguration.DefaultInstance.StringDefaultComparison, + (StringComparisonDefault.Unspecified, var global) => global.Value, + (var local, _) => local, + }; + var validationExceptionType = localValues.ValidationExceptionType ?? globalValues?.ValidationExceptionType ?? VogenConfiguration.DefaultInstance.ValidationExceptionType; @@ -193,7 +201,8 @@ public static VogenConfiguration CombineAndResolveAnythingUnspecified( OpenApiSchemaCustomizations: openApiSchemaCustomizations, ExplicitlySpecifyTypeInValueObject: primitiveTypeMustBeExplicit, PrimitiveEqualityGeneration: primitiveEqualityGeneration, - NumericsGeneration: numericsGeneration); + NumericsGeneration: numericsGeneration, + StringDefaultComparison: stringDefaultComparison); } /// If we don't have a global attribute, just use the default configuration as there diff --git a/src/Vogen/Diagnostics/DiagnosticsCatalogue.cs b/src/Vogen/Diagnostics/DiagnosticsCatalogue.cs index 0dba663c75b..1082286d589 100644 --- a/src/Vogen/Diagnostics/DiagnosticsCatalogue.cs +++ b/src/Vogen/Diagnostics/DiagnosticsCatalogue.cs @@ -135,6 +135,17 @@ internal static class DiagnosticsCatalogue "'{0}' has NumericsGeneration.Generate set, but the underlying type '{1}' does not implement INumberBase. No numeric interface will be generated.", DiagnosticSeverity.Warning); + private static readonly DiagnosticDescriptor _stringDefaultComparisonNotApplicable = CreateDescriptor( + RuleIdentifiers.StringDefaultComparisonNotApplicable, + "StringDefaultComparison has no effect", + "'{0}' has StringDefaultComparison set, but the underlying type '{1}' is not a string. StringDefaultComparison only applies to string-backed value objects.", + DiagnosticSeverity.Warning); + + private static readonly DiagnosticDescriptor _stringDefaultComparisonConflictsWithStringComparersOmit = CreateDescriptor( + RuleIdentifiers.StringDefaultComparisonConflictsWithStringComparersOmit, + "StringDefaultComparison conflicts with StringComparersGeneration.Omit", + "Cannot set stringComparers to Omit when stringDefaultComparison is specified, as the Comparers class must be generated to support the default comparison."); + public static Diagnostic TypeCannotBeNested(INamedTypeSymbol typeModel, INamedTypeSymbol container) => Create(_typeCannotBeNested, typeModel.Locations, typeModel.Name, container.Name); @@ -214,6 +225,12 @@ public static Diagnostic TypesReferencedInAConversionMarkerMustBeValueObjects(IN public static Diagnostic NumericsGenerationNotApplicable(INamedTypeSymbol voSymbol, ITypeSymbol underlyingType) => Create(_numericsGenerationNotApplicable, voSymbol.Locations, voSymbol.Name, underlyingType.Name); + public static Diagnostic StringDefaultComparisonNotApplicable(INamedTypeSymbol voSymbol, ITypeSymbol underlyingType) => + Create(_stringDefaultComparisonNotApplicable, voSymbol.Locations, voSymbol.Name, underlyingType.Name); + + public static Diagnostic StringDefaultComparisonConflictsWithStringComparersOmit(Location location) => + Diagnostic.Create(_stringDefaultComparisonConflictsWithStringComparersOmit, location); + private static DiagnosticDescriptor CreateDescriptor(string code, string title, string messageFormat, DiagnosticSeverity severity = DiagnosticSeverity.Error) { string[] tags = severity == DiagnosticSeverity.Error ? new[] { WellKnownDiagnosticTags.NotConfigurable } : Array.Empty(); diff --git a/src/Vogen/Diagnostics/RuleIdentifiers.cs b/src/Vogen/Diagnostics/RuleIdentifiers.cs index d58182018f0..b5a1604ee32 100644 --- a/src/Vogen/Diagnostics/RuleIdentifiers.cs +++ b/src/Vogen/Diagnostics/RuleIdentifiers.cs @@ -45,4 +45,6 @@ public static class RuleIdentifiers public const string BothImplicitAndExplicitCastsSpecified = "VOG036"; public const string NumericsGenerationNotApplicable = "VOG037"; public const string PropertyOfValueObjectShouldBeInitialized = "VOG038"; + public const string StringDefaultComparisonNotApplicable = "VOG039"; + public const string StringDefaultComparisonConflictsWithStringComparersOmit = "VOG040"; } \ No newline at end of file diff --git a/src/Vogen/GenerateCodeForEqualsMethodsAndOperators.cs b/src/Vogen/GenerateCodeForEqualsMethodsAndOperators.cs index bdc70270fbc..857a2e8afb9 100644 --- a/src/Vogen/GenerateCodeForEqualsMethodsAndOperators.cs +++ b/src/Vogen/GenerateCodeForEqualsMethodsAndOperators.cs @@ -13,7 +13,11 @@ public static string GenerateEqualsMethodsForAClass(VoWorkItem item, TypeDeclara string virtualKeyword = item is { IsRecordClass: true, IsSealed: false } ? "virtual " : " "; - var ret = item.UserProvidedOverloads.EqualsForWrapper.WasProvided ? string.Empty : + string classEqualityExpr = item.IsUnderlyingAString && item.Config.GetStringDefaultComparerExpression() is { } classComparer + ? $"{classComparer}.Equals(Value, other.Value)" + : $"global::System.Collections.Generic.EqualityComparer<{item.UnderlyingTypeFullNameWithGlobalAlias}>.Default.Equals(Value, other.Value)"; + + var ret = item.UserProvidedOverloads.EqualsForWrapper.WasProvided ? string.Empty : $$""" public {{virtualKeyword}}global::System.Boolean Equals({{className}}{{wrapperQ}} other) @@ -32,7 +36,7 @@ public static string GenerateEqualsMethodsForAClass(VoWorkItem item, TypeDeclara return true; } - return GetType() == other.GetType() && {{$"global::System.Collections.Generic.EqualityComparer<{item.UnderlyingTypeFullNameWithGlobalAlias}>.Default.Equals(Value, other.Value)"}}; + return GetType() == other.GetType() && {{classEqualityExpr}}; } """; @@ -65,6 +69,10 @@ public static string GenerateEqualsMethodsForAStruct(VoWorkItem item, TypeDeclar { var structName = tds.Identifier; + string structEqualityExpr = item.IsUnderlyingAString && item.Config.GetStringDefaultComparerExpression() is { } structComparer + ? $"{structComparer}.Equals(Value, other.Value)" + : $"global::System.Collections.Generic.EqualityComparer<{item.UnderlyingTypeFullNameWithGlobalAlias}>.Default.Equals(Value, other.Value)"; + var ret = item.UserProvidedOverloads.EqualsForWrapper.WasProvided ? string.Empty : $$""" public readonly global::System.Boolean Equals({{structName}} other) @@ -73,7 +81,7 @@ public static string GenerateEqualsMethodsForAStruct(VoWorkItem item, TypeDeclar // 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<{item.UnderlyingTypeFullNameWithGlobalAlias}>.Default.Equals(Value, other.Value)"}}; + return {{structEqualityExpr}}; } """; @@ -118,12 +126,16 @@ private static string GenerateEqualsMethodForPrimitive(VoWorkItem item, bool isR string readonlyOrEmpty = isReadOnly ? " readonly" : string.Empty; + string primitiveEqualityBody = isString && item.Config.GetStringDefaultComparerExpression() is { } primComparer + ? $"return {primComparer}.Equals(Value, primitive);" + : "return Value.Equals(primitive);"; + string output = item.UserProvidedOverloads.EqualsForUnderlying.WasProvided ? string.Empty : $$""" public{{readonlyOrEmpty}} global::System.Boolean Equals({{item.UnderlyingTypeFullNameWithGlobalAlias}}{{underlyingQ}} primitive) { - return Value.Equals(primitive); + {{primitiveEqualityBody}} } """; @@ -152,11 +164,23 @@ public static string GenerateEqualsOperatorsForPrimitivesIfNeeded(string itemUnd string w = item.Nullable.QuestionMarkForWrapper; string u = item.Nullable.QuestionMarkForUnderlying; - + + if (item.IsUnderlyingAString && item.Config.GetStringDefaultComparerExpression() is { } opComparer) + { + return $""" + + public static global::System.Boolean operator ==({typeName}{w} left, {itemUnderlyingType}{u} right) => {opComparer}.Equals(left{w}.Value, right); + public static global::System.Boolean operator ==({itemUnderlyingType}{u} left, {typeName}{w} right) => {opComparer}.Equals(left, right{w}.Value); + + public static global::System.Boolean operator !=({itemUnderlyingType}{u} left, {typeName}{w} right) => !(left == right); + public static global::System.Boolean operator !=({typeName}{w} left, {itemUnderlyingType}{u} right) => !(left == right); + """; + } + string wDefaultToFalse = item.Nullable.IsEnabled && item.IsTheWrapperAReferenceType ? "?? false" : string.Empty; return $""" - + public static global::System.Boolean operator ==({typeName}{w} left, {itemUnderlyingType}{u} right) => left{w}.Value.Equals(right) {wDefaultToFalse}; public static global::System.Boolean operator ==({itemUnderlyingType}{u} left, {typeName}{w} right) => right{w}.Value.Equals(left) {wDefaultToFalse}; diff --git a/src/Vogen/GenerateCodeForHashCodes.cs b/src/Vogen/GenerateCodeForHashCodes.cs index bdea6ceb97f..c6d6fc97dd5 100644 --- a/src/Vogen/GenerateCodeForHashCodes.cs +++ b/src/Vogen/GenerateCodeForHashCodes.cs @@ -16,13 +16,15 @@ public static string GenerateGetHashCodeForAClass(VoWorkItem item) // When the underlying type is a nullable value type (e.g. ushort?), EqualityComparer.Default.GetHashCode // has [DisallowNull] on its parameter in .NET 10+, causing CS8607. Use null-safe approach instead. // Wrap the expression in parentheses to ensure correct precedence with the ^ operator. - string hashCodeExpression = item.UnderlyingType.IsNullableValueType() - ? "(Value is null ? 0 : Value.GetHashCode())" - : $"global::System.Collections.Generic.EqualityComparer<{itemUnderlyingType}>.Default.GetHashCode(Value)"; + string hashCodeExpression = item.IsUnderlyingAString && item.Config.GetStringDefaultComparerExpression() is { } classHashComparer + ? $"{classHashComparer}.GetHashCode(Value)" + : item.UnderlyingType.IsNullableValueType() + ? "(Value is null ? 0 : Value.GetHashCode())" + : $"global::System.Collections.Generic.EqualityComparer<{itemUnderlyingType}>.Default.GetHashCode(Value)"; return $$""" - + public override global::System.Int32 GetHashCode() { unchecked // Overflow is fine, just wrap @@ -47,13 +49,15 @@ public static string GenerateForAStruct(VoWorkItem item) // When the underlying type is a nullable value type (e.g. ushort?), EqualityComparer.Default.GetHashCode // has [DisallowNull] on its parameter in .NET 10+, causing CS8607. Use null-safe approach instead. - string hashCodeExpression = item.UnderlyingType.IsNullableValueType() - ? "Value is null ? 0 : Value.GetHashCode()" - : $"global::System.Collections.Generic.EqualityComparer<{itemUnderlyingType}>.Default.GetHashCode(Value)"; + string hashCodeExpression = item.IsUnderlyingAString && item.Config.GetStringDefaultComparerExpression() is { } structHashComparer + ? $"{structHashComparer}.GetHashCode(Value)" + : item.UnderlyingType.IsNullableValueType() + ? "Value is null ? 0 : Value.GetHashCode()" + : $"global::System.Collections.Generic.EqualityComparer<{itemUnderlyingType}>.Default.GetHashCode(Value)"; return $$""" - + public readonly override global::System.Int32 GetHashCode() { return {{hashCodeExpression}}; diff --git a/src/Vogen/GenerateCodeForStringComparers.cs b/src/Vogen/GenerateCodeForStringComparers.cs index 0a57885290f..e6f56c9f9df 100644 --- a/src/Vogen/GenerateCodeForStringComparers.cs +++ b/src/Vogen/GenerateCodeForStringComparers.cs @@ -11,7 +11,10 @@ public static string GenerateIfNeeded(VoWorkItem item, TypeDeclarationSyntax tds return string.Empty; } - if(item.Config.StringComparers != StringComparersGeneration.Generate) + bool hasDefaultComparison = item.Config.StringDefaultComparison is not StringComparisonDefault.Unspecified + and not StringComparisonDefault.Omit; + + if (item.Config.StringComparers != StringComparersGeneration.Generate && !hasDefaultComparison) { return string.Empty; } @@ -35,9 +38,9 @@ public bool Equals({{tds.Identifier}} x, {{tds.Identifier}} y) return _comparer.Equals(x._value, y._value); } - public int GetHashCode({{tds.Identifier}} obj) + public int GetHashCode({{tds.Identifier}} obj) { - return _comparer.GetHashCode(); + return _comparer.GetHashCode(obj._value); } } diff --git a/src/Vogen/VogenConfiguration.cs b/src/Vogen/VogenConfiguration.cs index 41d052c7ecc..c031bf5ce5d 100644 --- a/src/Vogen/VogenConfiguration.cs +++ b/src/Vogen/VogenConfiguration.cs @@ -23,7 +23,8 @@ public record VogenConfiguration( OpenApiSchemaCustomizations OpenApiSchemaCustomizations, bool ExplicitlySpecifyTypeInValueObject, PrimitiveEqualityGeneration PrimitiveEqualityGeneration, - NumericsGeneration NumericsGeneration) + NumericsGeneration NumericsGeneration, + StringComparisonDefault StringDefaultComparison) { // Don't add default values here, they should be in DefaultInstance. @@ -52,5 +53,17 @@ public record VogenConfiguration( OpenApiSchemaCustomizations: OpenApiSchemaCustomizations.Omit, ExplicitlySpecifyTypeInValueObject: false, PrimitiveEqualityGeneration: PrimitiveEqualityGeneration.GenerateOperatorsAndMethods, - NumericsGeneration: NumericsGeneration.Omit); + NumericsGeneration: NumericsGeneration.Omit, + StringDefaultComparison: StringComparisonDefault.Omit); + + public string? GetStringDefaultComparerExpression() => StringDefaultComparison switch + { + StringComparisonDefault.Ordinal => "global::System.StringComparer.Ordinal", + StringComparisonDefault.OrdinalIgnoreCase => "global::System.StringComparer.OrdinalIgnoreCase", + StringComparisonDefault.CurrentCulture => "global::System.StringComparer.CurrentCulture", + StringComparisonDefault.CurrentCultureIgnoreCase => "global::System.StringComparer.CurrentCultureIgnoreCase", + StringComparisonDefault.InvariantCulture => "global::System.StringComparer.InvariantCulture", + StringComparisonDefault.InvariantCultureIgnoreCase => "global::System.StringComparer.InvariantCultureIgnoreCase", + _ => null + }; } diff --git a/tests/ConsumerTests/StringComparisons/ForClasses.cs b/tests/ConsumerTests/StringComparisons/ForClasses.cs index 106cf2b3bbb..d43bfec78b0 100644 --- a/tests/ConsumerTests/StringComparisons/ForClasses.cs +++ b/tests/ConsumerTests/StringComparisons/ForClasses.cs @@ -55,6 +55,24 @@ public void OrdinalIgnoreCase_Generic() (left == right).Should().BeFalse(); } + [Fact] + public void StringDefaultComparison_OrdinalIgnoreCase() + { + using var _ = new AssertionScope(); + + StringVo_Class_DefaultOrdinalIgnoreCase left = StringVo_Class_DefaultOrdinalIgnoreCase.From("abc"); + StringVo_Class_DefaultOrdinalIgnoreCase right = StringVo_Class_DefaultOrdinalIgnoreCase.From("ABC"); + + (left == right).Should().BeTrue(); + left.Equals(right).Should().BeTrue(); + left.Equals("ABC").Should().BeTrue(); + left.GetHashCode().Should().Be(right.GetHashCode()); + + Dictionary d = new(); + d.Add(StringVo_Class_DefaultOrdinalIgnoreCase.From("one"), 1); + d.ContainsKey(StringVo_Class_DefaultOrdinalIgnoreCase.From("ONE")).Should().BeTrue(); + } + [Fact] public void OrdinalIgnoreCase_in_a_dictionary() { diff --git a/tests/ConsumerTests/StringComparisons/ForRecordClasses.cs b/tests/ConsumerTests/StringComparisons/ForRecordClasses.cs index 6786b3e5c72..9d59346ea87 100644 --- a/tests/ConsumerTests/StringComparisons/ForRecordClasses.cs +++ b/tests/ConsumerTests/StringComparisons/ForRecordClasses.cs @@ -55,6 +55,24 @@ public void OrdinalIgnoreCase_Generic() (left == right).Should().BeFalse(); } + [Fact] + public void StringDefaultComparison_OrdinalIgnoreCase() + { + using var _ = new AssertionScope(); + + StringVo_RecordClass_DefaultOrdinalIgnoreCase left = StringVo_RecordClass_DefaultOrdinalIgnoreCase.From("abc"); + StringVo_RecordClass_DefaultOrdinalIgnoreCase right = StringVo_RecordClass_DefaultOrdinalIgnoreCase.From("ABC"); + + (left == right).Should().BeTrue(); + left.Equals(right).Should().BeTrue(); + left.Equals("ABC").Should().BeTrue(); + left.GetHashCode().Should().Be(right.GetHashCode()); + + Dictionary d = new(); + d.Add(StringVo_RecordClass_DefaultOrdinalIgnoreCase.From("one"), 1); + d.ContainsKey(StringVo_RecordClass_DefaultOrdinalIgnoreCase.From("ONE")).Should().BeTrue(); + } + [Fact] public void OrdinalIgnoreCase_in_a_dictionary() { diff --git a/tests/ConsumerTests/StringComparisons/ForRecordStructs.cs b/tests/ConsumerTests/StringComparisons/ForRecordStructs.cs index d949613fc44..b91b5de424b 100644 --- a/tests/ConsumerTests/StringComparisons/ForRecordStructs.cs +++ b/tests/ConsumerTests/StringComparisons/ForRecordStructs.cs @@ -58,6 +58,24 @@ public void OrdinalIgnoreCase_Generic() (left == right).Should().BeFalse(); } + [Fact] + public void StringDefaultComparison_OrdinalIgnoreCase() + { + using var _ = new AssertionScope(); + + StringVo_RecordStruct_DefaultOrdinalIgnoreCase left = StringVo_RecordStruct_DefaultOrdinalIgnoreCase.From("abc"); + StringVo_RecordStruct_DefaultOrdinalIgnoreCase right = StringVo_RecordStruct_DefaultOrdinalIgnoreCase.From("ABC"); + + (left == right).Should().BeTrue(); + left.Equals(right).Should().BeTrue(); + left.Equals("ABC").Should().BeTrue(); + left.GetHashCode().Should().Be(right.GetHashCode()); + + Dictionary d = new(); + d.Add(StringVo_RecordStruct_DefaultOrdinalIgnoreCase.From("one"), 1); + d.ContainsKey(StringVo_RecordStruct_DefaultOrdinalIgnoreCase.From("ONE")).Should().BeTrue(); + } + [Fact] public void OrdinalIgnoreCase_in_a_dictionary() { diff --git a/tests/ConsumerTests/StringComparisons/ForStructs.cs b/tests/ConsumerTests/StringComparisons/ForStructs.cs index 0d29d5debc6..f8676bebf25 100644 --- a/tests/ConsumerTests/StringComparisons/ForStructs.cs +++ b/tests/ConsumerTests/StringComparisons/ForStructs.cs @@ -71,6 +71,24 @@ public void OrdinalIgnoreCase_Generic() (left == right).Should().BeFalse(); } + [Fact] + public void StringDefaultComparison_OrdinalIgnoreCase() + { + using var _ = new AssertionScope(); + + StringVo_Struct_DefaultOrdinalIgnoreCase left = StringVo_Struct_DefaultOrdinalIgnoreCase.From("abc"); + StringVo_Struct_DefaultOrdinalIgnoreCase right = StringVo_Struct_DefaultOrdinalIgnoreCase.From("ABC"); + + (left == right).Should().BeTrue(); + left.Equals(right).Should().BeTrue(); + left.Equals("ABC").Should().BeTrue(); + left.GetHashCode().Should().Be(right.GetHashCode()); + + Dictionary d = new(); + d.Add(StringVo_Struct_DefaultOrdinalIgnoreCase.From("one"), 1); + d.ContainsKey(StringVo_Struct_DefaultOrdinalIgnoreCase.From("ONE")).Should().BeTrue(); + } + [Fact] public void OrdinalIgnoreCase_in_a_dictionary() { diff --git a/tests/ConsumerTests/StringComparisons/Types.cs b/tests/ConsumerTests/StringComparisons/Types.cs index cbf16506b24..238aff0ac93 100644 --- a/tests/ConsumerTests/StringComparisons/Types.cs +++ b/tests/ConsumerTests/StringComparisons/Types.cs @@ -90,3 +90,23 @@ public partial record struct StringVo_RecordStruct_Generic public partial struct StringVo_Struct_Generic { } + +[ValueObject(typeof(string), stringDefaultComparison: StringComparisonDefault.OrdinalIgnoreCase)] +public partial class StringVo_Class_DefaultOrdinalIgnoreCase +{ +} + +[ValueObject(typeof(string), stringDefaultComparison: StringComparisonDefault.OrdinalIgnoreCase)] +public partial record class StringVo_RecordClass_DefaultOrdinalIgnoreCase +{ +} + +[ValueObject(typeof(string), stringDefaultComparison: StringComparisonDefault.OrdinalIgnoreCase)] +public partial record struct StringVo_RecordStruct_DefaultOrdinalIgnoreCase +{ +} + +[ValueObject(typeof(string), stringDefaultComparison: StringComparisonDefault.OrdinalIgnoreCase)] +public partial struct StringVo_Struct_DefaultOrdinalIgnoreCase +{ +} diff --git a/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs b/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs index 26d7003cb35..cf723699278 100644 --- a/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs +++ b/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs @@ -39,6 +39,23 @@ public partial class StringThing { } .RunOnAllFrameworks(); } + [Fact] + public Task Generates_with_default_comparison_OrdinalIgnoreCase() + { + string source = $$""" + using System; + using Vogen; + namespace Whatever; + + [ValueObject(typeof(string), stringDefaultComparison: StringComparisonDefault.OrdinalIgnoreCase)] + public partial class StringThing { } + """; + + return new SnapshotRunner() + .WithSource(source) + .RunOnAllFrameworks(); + } + [Fact] public Task Does_not_generate_the_equals_method_that_takes_a_StringComparison_when_not_a_string() { diff --git a/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_when_specified.verified.txt b/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_when_specified.verified.txt index 351990388f5..26f2975c54d 100644 --- a/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_when_specified.verified.txt +++ b/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_when_specified.verified.txt @@ -208,7 +208,7 @@ private readonly global::System.Diagnostics.StackTrace _stackTrace = null!; public int GetHashCode(StringThing obj) { - return _comparer.GetHashCode(); + return _comparer.GetHashCode(obj._value); } } diff --git a/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt b/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt new file mode 100644 index 00000000000..62b4cd9b562 --- /dev/null +++ b/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt @@ -0,0 +1,620 @@ +[ +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace generator +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + public class VogenTypesFactory : global::System.Text.Json.Serialization.JsonConverterFactory + { + public VogenTypesFactory() + { + } + + private static readonly global::System.Collections.Generic.Dictionary> _lookup = new global::System.Collections.Generic.Dictionary> + { + { + typeof(global::Whatever.StringThing), + new global::System.Lazy(() => new global::Whatever.StringThing.StringThingSystemTextJsonConverter()) + } + }; + public override bool CanConvert(global::System.Type typeToConvert) => _lookup.ContainsKey(typeToConvert); + public override global::System.Text.Json.Serialization.JsonConverter CreateConverter(global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) => _lookup[typeToConvert].Value; + } +} + +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace Whatever +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + [global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] + [global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] + [global::System.Diagnostics.DebuggerTypeProxyAttribute(typeof(StringThingDebugView))] + [global::System.Diagnostics.DebuggerDisplayAttribute("Underlying type: global::System.String, Value = { _value }")] + public partial class StringThing : global::System.IEquatable, global::System.IEquatable, global::System.IComparable, global::System.IComparable, global::System.IParsable, global::System.IConvertible + { +#if DEBUG +private readonly global::System.Diagnostics.StackTrace _stackTrace = null!; +#endif +#if !VOGEN_NO_VALIDATION + private readonly global::System.Boolean _isInitialized; +#endif + private readonly global::System.String _value; + /// + /// Gets the underlying value if set, otherwise a is thrown. + /// + public global::System.String Value + { + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get + { + EnsureInitialized(); + return _value; + } + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public StringThing() + { +#if DEBUG + _stackTrace = new global::System.Diagnostics.StackTrace(); +#endif +#if !VOGEN_NO_VALIDATION + _isInitialized = false; +#endif + _value = default; + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private StringThing(global::System.String value) + { + _value = value; +#if !VOGEN_NO_VALIDATION + _isInitialized = true; +#endif + } + + /// + /// Builds an instance from the provided underlying type. + /// + /// The underlying type. + /// An instance of this type. + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static StringThing From(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + /// + /// Tries to build an instance from the provided underlying type. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, false will be returned. + /// + /// The underlying type. + /// An instance of the value object. + /// True if the value object can be built, otherwise false. + public static bool TryFrom( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String value, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing vo) + { + if (value is null) + { + vo = default; + return false; + } + + vo = new StringThing(value); + return true; + } + + /// + /// Tries to build an instance from the provided underlying value. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, an error will be returned. + /// + /// The primitive value. + /// A containing either the value object, or an error. + public static global::Vogen.ValueObjectOrError TryFrom(global::System.String value) + { + if (value is null) + { + return new global::Vogen.ValueObjectOrError(global::Vogen.Validation.Invalid("The value provided was null")); + } + + return new global::Vogen.ValueObjectOrError(new StringThing(value)); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] +#if VOGEN_NO_VALIDATION +#pragma warning disable CS8775 + public bool IsInitialized() => true; +#pragma warning restore CS8775 +#else + public bool IsInitialized() => _isInitialized; +#endif +#nullable disable + public static class Comparers + { + private class __StringEqualityComparer : global::System.Collections.Generic.IEqualityComparer + { + readonly global::System.StringComparer _comparer; + public __StringEqualityComparer(global::System.StringComparer comparer) + { + _comparer = comparer; + } + + public bool Equals(StringThing x, StringThing y) + { + return _comparer.Equals(x._value, y._value); + } + + public int GetHashCode(StringThing obj) + { + return _comparer.GetHashCode(obj._value); + } + } + + public static global::System.Collections.Generic.IEqualityComparer Ordinal => new __StringEqualityComparer(global::System.StringComparer.Ordinal); + public static global::System.Collections.Generic.IEqualityComparer OrdinalIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.OrdinalIgnoreCase); + public static global::System.Collections.Generic.IEqualityComparer CurrentCulture => new __StringEqualityComparer(global::System.StringComparer.CurrentCulture); + public static global::System.Collections.Generic.IEqualityComparer CurrentCultureIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.CurrentCultureIgnoreCase); + public static global::System.Collections.Generic.IEqualityComparer InvariantCulture => new __StringEqualityComparer(global::System.StringComparer.InvariantCulture); + public static global::System.Collections.Generic.IEqualityComparer InvariantCultureIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.InvariantCultureIgnoreCase); +#nullable restore + } + + // only called internally when something has been deserialized into + // its primitive type. + private static StringThing __Deserialize(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + public global::System.Boolean Equals(StringThing other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + // It's possible to create uninitialized instances via converters such as EfCore (HasDefaultValue), which call Equals. + // We treat anything uninitialized as not equal to anything, even other uninitialized instances of this type. + if (!IsInitialized() || !other.IsInitialized()) + return false; + if (ReferenceEquals(this, other)) + { + return true; + } + + return GetType() == other.GetType() && global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, other.Value); + } + + public global::System.Boolean Equals(StringThing other, global::System.Collections.Generic.IEqualityComparer comparer) + { + return comparer.Equals(this, other); + } + + public global::System.Boolean Equals(global::System.String primitive) + { + return global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, primitive); + } + + public global::System.Boolean Equals(global::System.String primitive, global::System.StringComparer comparer) + { + return comparer.Equals(Value, primitive); + } + + public override global::System.Boolean Equals(global::System.Object obj) + { + return Equals(obj as StringThing); + } + + public static global::System.Boolean operator ==(StringThing left, StringThing right) => Equals(left, right); + public static global::System.Boolean operator !=(StringThing left, StringThing right) => !Equals(left, right); + public static global::System.Boolean operator ==(StringThing left, global::System.String right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left.Value, right); + public static global::System.Boolean operator ==(global::System.String left, StringThing right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left, right.Value); + public static global::System.Boolean operator !=(global::System.String left, StringThing right) => !(left == right); + public static global::System.Boolean operator !=(StringThing left, global::System.String right) => !(left == right); + public static explicit operator StringThing(global::System.String value) => From(value); + public static explicit operator global::System.String(StringThing value) => value.Value; + public int CompareTo(StringThing other) + { + if (other is null) + return 1; + return Value.CompareTo(other.Value); + } + + public int CompareTo(object other) + { + if (other is null) + return 1; + if (other is StringThing x) + return CompareTo(x); + ThrowHelper.ThrowArgumentException("Cannot compare to object as it is not of type StringThing", nameof(other)); + return 0; + } + + /// + /// + /// + /// True if the value passes any validation (after running any optional normalization). + /// + public static global::System.Boolean TryParse( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String s, global::System.IFormatProvider provider, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing result) + { + if (s is null) + { + result = default; + return false; + } + + result = new StringThing(s); + return true; + } + + /// + /// + /// + /// The value created via the method. + /// + /// Thrown when the value can be parsed, but is not valid. + public static StringThing Parse(global::System.String s, global::System.IFormatProvider provider) + { + return From(s); + } + +#nullable disable +#nullable restore +#nullable disable + /// + public System.TypeCode GetTypeCode() + { + return IsInitialized() ? Value.GetTypeCode() : default; + } + + /// + bool System.IConvertible.ToBoolean(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToBoolean(provider) : default; + } + + /// + byte System.IConvertible.ToByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToByte(provider) : default; + } + + /// + char System.IConvertible.ToChar(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToChar(provider) : default; + } + + /// + System.DateTime System.IConvertible.ToDateTime(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDateTime(provider) : default; + } + + /// + decimal System.IConvertible.ToDecimal(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDecimal(provider) : default; + } + + /// + double System.IConvertible.ToDouble(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDouble(provider) : default; + } + + /// + short System.IConvertible.ToInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt16(provider) : default; + } + + /// + int System.IConvertible.ToInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt32(provider) : default; + } + + /// + long System.IConvertible.ToInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt64(provider) : default; + } + + /// + sbyte System.IConvertible.ToSByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSByte(provider) : default; + } + + /// + float System.IConvertible.ToSingle(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSingle(provider) : default; + } + + /// + object System.IConvertible.ToType(global::System.Type @type, global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToType(@type, provider) : default; + } + + /// + ushort System.IConvertible.ToUInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt16(provider) : default; + } + + /// + uint System.IConvertible.ToUInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt32(provider) : default; + } + + /// + ulong System.IConvertible.ToUInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt64(provider) : default; + } + +#nullable restore + public override global::System.Int32 GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + global::System.Int32 hash = (global::System.Int32)2166136261; + hash = (hash * 16777619) ^ GetType().GetHashCode(); + hash = (hash * 16777619) ^ global::System.StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + return hash; + } + } + + /// + public override global::System.String ToString() => IsInitialized() ? Value.ToString() ?? "" : "[UNINITIALIZED]"; + /// + public global::System.String ToString(global::System.IFormatProvider provider) => IsInitialized() ? Value.ToString(provider) ?? "" : "[UNINITIALIZED]"; + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private void EnsureInitialized() + { + if (!IsInitialized()) + { +#if DEBUG + ThrowHelper.ThrowWhenNotInitialized(_stackTrace); +#else + ThrowHelper.ThrowWhenNotInitialized(); +#endif + } + } + +#nullable disable + /// + /// Converts a StringThing to or from JSON. + /// + public partial class StringThingSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override StringThing Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + +#if NET6_0_OR_GREATER + public override StringThing ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WritePropertyName(value.Value); + } +#endif +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + private static void ThrowJsonExceptionWhenValidationFails(global::Vogen.Validation validation) + { + var e = ThrowHelper.CreateValidationException(validation); + throw new global::System.Text.Json.JsonException(null, e); + } + + private static void ThrowJsonExceptionWhenNull(global::System.String value) + { + if (value == null) + { + var e = ThrowHelper.CreateCannotBeNullException(); + throw new global::System.Text.Json.JsonException(null, e); + } + } + + private static StringThing DeserializeJson(global::System.String value) + { + ThrowJsonExceptionWhenNull(value); + return new StringThing(value); + } + } + +#nullable restore +#nullable disable + class StringThingTypeConverter : global::System.ComponentModel.TypeConverter + { + public override global::System.Boolean CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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) + { + var stringValue = value as global::System.String; + if (stringValue != null) + { + return StringThing.__Deserialize(stringValue); + } + + return base.ConvertFrom(context, culture, value); + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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 StringThing idValue) + { + if (destinationType == typeof(global::System.String)) + { + return idValue.Value; + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + +#nullable restore + internal sealed class StringThingDebugView + { + private readonly StringThing _t; + StringThingDebugView(StringThing t) + { + _t = t; + } + + public global::System.String UnderlyingType => "global::System.String"; + public global::System.String Value => _t.Value; + public global::System.String Conversions => @"[global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] +[global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] +"; + } + + static class ThrowHelper + { +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowInvalidOperationException(string message) => throw new global::System.InvalidOperationException(message); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowArgumentException(string message, string arg) => throw new global::System.ArgumentException(message, arg); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenCreatedWithNull() => throw CreateCannotBeNullException(); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized() => throw CreateValidationException("Use of uninitialized Value Object."); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized(global::System.Diagnostics.StackTrace stackTrace) => throw CreateValidationException("Use of uninitialized Value Object at: " + stackTrace ?? ""); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenValidationFails(global::Vogen.Validation validation) + { + throw CreateValidationException(validation); + } + + internal static global::System.Exception CreateValidationException(string message) => new global::Vogen.ValueObjectValidationException(message); + internal static global::System.Exception CreateCannotBeNullException() => new global::Vogen.ValueObjectValidationException("Cannot create a value object with null."); + internal static global::System.Exception CreateValidationException(global::Vogen.Validation validation) + { + var ex = CreateValidationException(validation.ErrorMessage); + if (validation.Data != null) + { + foreach (var kvp in validation.Data) + { + ex.Data[kvp.Key] = kvp.Value; + } + } + + return ex; + } + } + } +} +] \ No newline at end of file diff --git a/tests/Vogen.Tests/ConfigurationTests/VogenConfigurationTests.cs b/tests/Vogen.Tests/ConfigurationTests/VogenConfigurationTests.cs index 541c8153692..fabe071abbf 100644 --- a/tests/Vogen.Tests/ConfigurationTests/VogenConfigurationTests.cs +++ b/tests/Vogen.Tests/ConfigurationTests/VogenConfigurationTests.cs @@ -74,7 +74,8 @@ private static VogenConfiguration ConfigWithOmitDebugAs(DebuggerAttributeGenerat OpenApiSchemaCustomizations.Unspecified, false, PrimitiveEqualityGeneration.GenerateOperatorsAndMethods, - NumericsGeneration.Omit); + NumericsGeneration.Omit, + StringComparisonDefault.Omit); } public class Primitive_equality_generation @@ -150,7 +151,8 @@ private static VogenConfiguration ConfigWithCastingAs(CastOperator toPrimitiveCa OpenApiSchemaCustomizations: OpenApiSchemaCustomizations.Unspecified, false, PrimitiveEqualityGeneration.GenerateOperatorsAndMethods, - NumericsGeneration.Omit); + NumericsGeneration.Omit, + StringComparisonDefault.Omit); } public class Conversion @@ -256,7 +258,8 @@ private static VogenConfiguration ConfigWithOmitConversionsAs(Conversions conver OpenApiSchemaCustomizations.Unspecified, false, PrimitiveEqualityGeneration.GenerateOperatorsAndMethods, - NumericsGeneration.Omit); + NumericsGeneration.Omit, + StringComparisonDefault.Omit); } public class Comparable From b6c4d29d3d69b5ae6f6b2b7e906bde35430b263e Mon Sep 17 00:00:00 2001 From: Sir Phillip Tubell Date: Wed, 17 Jun 2026 21:20:29 -0400 Subject: [PATCH 2/5] fix: updated the secondary constructors so they do not class with the previous ones to maintain backwards compatibility --- src/Vogen.SharedTypes/ValueObjectAttribute.cs | 68 +++++++++---------- .../VogenDefaultsAttribute.cs | 43 ++++++------ .../ConsumerTests/StringComparisons/Types.cs | 8 +-- .../StringComparisonGenerationTests.cs | 2 +- 4 files changed, 60 insertions(+), 61 deletions(-) diff --git a/src/Vogen.SharedTypes/ValueObjectAttribute.cs b/src/Vogen.SharedTypes/ValueObjectAttribute.cs index 1d89feb5c44..a84e365cbae 100644 --- a/src/Vogen.SharedTypes/ValueObjectAttribute.cs +++ b/src/Vogen.SharedTypes/ValueObjectAttribute.cs @@ -125,22 +125,22 @@ public ValueObjectAttribute( /// Specifies whether to generate numeric interfaces (INumber<T> or INumberBase<T> depending on the underlying type)—defaults to . /// Specifies the default used for ==, Equals, and GetHashCode on string-backed value objects—defaults to which uses the underlying string's default comparison. public ValueObjectAttribute( - Conversions conversions = Conversions.Unspecified, - Type? throws = null!, - Customizations customizations = Customizations.None, - DeserializationStrictness deserializationStrictness = DeserializationStrictness.AllowValidAndKnownInstances, - DebuggerAttributeGeneration debuggerAttributes = DebuggerAttributeGeneration.Default, - ComparisonGeneration comparison = ComparisonGeneration.Default, - StringComparersGeneration stringComparers = StringComparersGeneration.Unspecified, - CastOperator toPrimitiveCasting = CastOperator.Unspecified, - CastOperator fromPrimitiveCasting = CastOperator.Unspecified, - ParsableForStrings parsableForStrings = ParsableForStrings.Unspecified, - ParsableForPrimitives parsableForPrimitives = ParsableForPrimitives.Unspecified, - TryFromGeneration tryFromGeneration = TryFromGeneration.Unspecified, - IsInitializedMethodGeneration isInitializedMethodGeneration = IsInitializedMethodGeneration.Unspecified, - PrimitiveEqualityGeneration primitiveEqualityGeneration = PrimitiveEqualityGeneration.Unspecified, - NumericsGeneration numericsGeneration = NumericsGeneration.Unspecified, - StringComparisonDefault stringDefaultComparison = StringComparisonDefault.Unspecified) : base( + Conversions conversions, + Type? throws, + Customizations customizations, + DeserializationStrictness deserializationStrictness, + DebuggerAttributeGeneration debuggerAttributes, + ComparisonGeneration comparison, + StringComparersGeneration stringComparers, + CastOperator toPrimitiveCasting, + CastOperator fromPrimitiveCasting, + ParsableForStrings parsableForStrings, + ParsableForPrimitives parsableForPrimitives, + TryFromGeneration tryFromGeneration, + IsInitializedMethodGeneration isInitializedMethodGeneration, + PrimitiveEqualityGeneration primitiveEqualityGeneration, + NumericsGeneration numericsGeneration, + StringComparisonDefault stringDefaultComparison) : base( typeof(T), conversions, throws, @@ -174,7 +174,7 @@ public class ValueObjectAttribute : Attribute // using parameter indexes (i.e. it expected param 0 to be the underlying type etc). // ReSharper disable once MemberCanBeProtected.Global - + /// /// Configures aspects of this individual value object. /// @@ -261,23 +261,23 @@ public ValueObjectAttribute( /// Specifies whether to generate numeric interfaces (INumber<T> or INumberBase<T> depending on the underlying type)—defaults to . /// Specifies the default used for ==, Equals, and GetHashCode on string-backed value objects—defaults to which uses the underlying string's default comparison. public ValueObjectAttribute( - Type? underlyingType = null!, - Conversions conversions = Conversions.Unspecified, - Type? throws = null!, - Customizations customizations = Customizations.None, - DeserializationStrictness deserializationStrictness = DeserializationStrictness.AllowValidAndKnownInstances, - DebuggerAttributeGeneration debuggerAttributes = DebuggerAttributeGeneration.Default, - ComparisonGeneration comparison = ComparisonGeneration.Default, - StringComparersGeneration stringComparers = StringComparersGeneration.Unspecified, - CastOperator toPrimitiveCasting = CastOperator.Unspecified, - CastOperator fromPrimitiveCasting = CastOperator.Unspecified, - ParsableForStrings parsableForStrings = ParsableForStrings.Unspecified, - ParsableForPrimitives parsableForPrimitives = ParsableForPrimitives.Unspecified, - TryFromGeneration tryFromGeneration = TryFromGeneration.Unspecified, - IsInitializedMethodGeneration isInitializedMethodGeneration = IsInitializedMethodGeneration.Unspecified, - PrimitiveEqualityGeneration primitiveEqualityGeneration = PrimitiveEqualityGeneration.Unspecified, - NumericsGeneration numericsGeneration = NumericsGeneration.Unspecified, - StringComparisonDefault stringDefaultComparison = StringComparisonDefault.Unspecified) + Type? underlyingType, + Conversions conversions, + Type? throws, + Customizations customizations, + DeserializationStrictness deserializationStrictness, + DebuggerAttributeGeneration debuggerAttributes, + ComparisonGeneration comparison, + StringComparersGeneration stringComparers, + CastOperator toPrimitiveCasting, + CastOperator fromPrimitiveCasting, + ParsableForStrings parsableForStrings, + ParsableForPrimitives parsableForPrimitives, + TryFromGeneration tryFromGeneration, + IsInitializedMethodGeneration isInitializedMethodGeneration, + PrimitiveEqualityGeneration primitiveEqualityGeneration, + NumericsGeneration numericsGeneration, + StringComparisonDefault stringDefaultComparison) { // DO NOT ADD PARAMETERS HERE, INSTEAD, CREATE OVERLOADS (at least until a new major version). // This is because some users use reflection to find this attribute, and changing the amount diff --git a/src/Vogen.SharedTypes/VogenDefaultsAttribute.cs b/src/Vogen.SharedTypes/VogenDefaultsAttribute.cs index 90cef74d8b0..2c4aeed3695 100644 --- a/src/Vogen.SharedTypes/VogenDefaultsAttribute.cs +++ b/src/Vogen.SharedTypes/VogenDefaultsAttribute.cs @@ -1,4 +1,4 @@ -// ReSharper disable MemberInitializerValueIgnored +// ReSharper disable MemberInitializerValueIgnored // ReSharper disable UnusedType.Global // ReSharper disable UnusedParameter.Local @@ -118,27 +118,26 @@ public VogenDefaultsAttribute( /// Specifies whether to generate numeric interfaces (INumber<T> or INumberBase<T> depending on the underlying type)—defaults to . /// Specifies the default used for ==, Equals, and GetHashCode on string-backed value objects—defaults to which uses the underlying string's default comparison. public VogenDefaultsAttribute( - Type? underlyingType = null, - Conversions conversions = Conversions.Unspecified, - Type? throws = null, - Customizations customizations = Customizations.None, - DeserializationStrictness deserializationStrictness = DeserializationStrictness.AllowValidAndKnownInstances, - DebuggerAttributeGeneration debuggerAttributes = DebuggerAttributeGeneration.Default, - CastOperator toPrimitiveCasting = CastOperator.Explicit, - CastOperator fromPrimitiveCasting = CastOperator.Explicit, - bool disableStackTraceRecordingInDebug = false, - ParsableForStrings parsableForStrings = ParsableForStrings.GenerateMethodsAndInterface, - ParsableForPrimitives parsableForPrimitives = ParsableForPrimitives.HoistMethodsAndInterfaces, - TryFromGeneration tryFromGeneration = TryFromGeneration.Unspecified, - IsInitializedMethodGeneration isInitializedMethodGeneration = IsInitializedMethodGeneration.Unspecified, - SystemTextJsonConverterFactoryGeneration systemTextJsonConverterFactoryGeneration = - SystemTextJsonConverterFactoryGeneration.Unspecified, - StaticAbstractsGeneration staticAbstractsGeneration = StaticAbstractsGeneration.Unspecified, - OpenApiSchemaCustomizations openApiSchemaCustomizations = OpenApiSchemaCustomizations.Unspecified, - bool explicitlySpecifyTypeInValueObject = false, - PrimitiveEqualityGeneration primitiveEqualityGeneration = PrimitiveEqualityGeneration.Unspecified, - NumericsGeneration numericsGeneration = NumericsGeneration.Unspecified, - StringComparisonDefault stringDefaultComparison = StringComparisonDefault.Unspecified) + Type? underlyingType, + Conversions conversions, + Type? throws, + Customizations customizations, + DeserializationStrictness deserializationStrictness, + DebuggerAttributeGeneration debuggerAttributes, + CastOperator toPrimitiveCasting, + CastOperator fromPrimitiveCasting, + bool disableStackTraceRecordingInDebug, + ParsableForStrings parsableForStrings, + ParsableForPrimitives parsableForPrimitives, + TryFromGeneration tryFromGeneration, + IsInitializedMethodGeneration isInitializedMethodGeneration, + SystemTextJsonConverterFactoryGeneration systemTextJsonConverterFactoryGeneration, + StaticAbstractsGeneration staticAbstractsGeneration, + OpenApiSchemaCustomizations openApiSchemaCustomizations, + bool explicitlySpecifyTypeInValueObject, + PrimitiveEqualityGeneration primitiveEqualityGeneration, + NumericsGeneration numericsGeneration, + StringComparisonDefault stringDefaultComparison) { } } \ No newline at end of file diff --git a/tests/ConsumerTests/StringComparisons/Types.cs b/tests/ConsumerTests/StringComparisons/Types.cs index 238aff0ac93..54c664d2f23 100644 --- a/tests/ConsumerTests/StringComparisons/Types.cs +++ b/tests/ConsumerTests/StringComparisons/Types.cs @@ -91,22 +91,22 @@ public partial struct StringVo_Struct_Generic { } -[ValueObject(typeof(string), stringDefaultComparison: StringComparisonDefault.OrdinalIgnoreCase)] +[ValueObject(typeof(string), Conversions.Unspecified, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] public partial class StringVo_Class_DefaultOrdinalIgnoreCase { } -[ValueObject(typeof(string), stringDefaultComparison: StringComparisonDefault.OrdinalIgnoreCase)] +[ValueObject(typeof(string), Conversions.Unspecified, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] public partial record class StringVo_RecordClass_DefaultOrdinalIgnoreCase { } -[ValueObject(typeof(string), stringDefaultComparison: StringComparisonDefault.OrdinalIgnoreCase)] +[ValueObject(typeof(string), Conversions.Unspecified, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] public partial record struct StringVo_RecordStruct_DefaultOrdinalIgnoreCase { } -[ValueObject(typeof(string), stringDefaultComparison: StringComparisonDefault.OrdinalIgnoreCase)] +[ValueObject(typeof(string), Conversions.Unspecified, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] public partial struct StringVo_Struct_DefaultOrdinalIgnoreCase { } diff --git a/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs b/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs index cf723699278..9640dd645da 100644 --- a/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs +++ b/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs @@ -47,7 +47,7 @@ public Task Generates_with_default_comparison_OrdinalIgnoreCase() using Vogen; namespace Whatever; - [ValueObject(typeof(string), stringDefaultComparison: StringComparisonDefault.OrdinalIgnoreCase)] + [ValueObject(typeof(string), Conversions.Default, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] public partial class StringThing { } """; From aa555e6afb2942174ccfb859b7c3a0f348c5b76e Mon Sep 17 00:00:00 2001 From: Sir Phillip Tubell Date: Thu, 18 Jun 2026 08:25:38 -0400 Subject: [PATCH 3/5] fix: decouple stringDefaultComparison from Comparers class generation Previously, setting stringDefaultComparison (without stringComparers) caused the Comparers nested class to be generated as an unintended side-effect. The two settings are orthogonal: stringComparers controls whether the Comparers utility class is generated; stringDefaultComparison controls the VO's own Equals/GetHashCode/operator behavior. The Comparers class now only generates when stringComparers = Generate is explicitly set. VOG040 (which blocked stringComparers = Omit alongside stringDefaultComparison) has been removed as its premise no longer holds. Added a snapshot test covering both settings used together. --- src/Vogen/BuildConfigurationFromAttributes.cs | 7 - src/Vogen/Diagnostics/DiagnosticsCatalogue.cs | 8 - src/Vogen/Diagnostics/RuleIdentifiers.cs | 1 - src/Vogen/GenerateCodeForStringComparers.cs | 5 +- .../StringComparisonGenerationTests.cs | 17 + ...rison_when_both_are_specified.verified.txt | 620 ++++++++++++++++++ ..._comparison_OrdinalIgnoreCase.verified.txt | 31 - 7 files changed, 638 insertions(+), 51 deletions(-) create mode 100644 tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt diff --git a/src/Vogen/BuildConfigurationFromAttributes.cs b/src/Vogen/BuildConfigurationFromAttributes.cs index 0b1765d01d1..6466c06ffa4 100644 --- a/src/Vogen/BuildConfigurationFromAttributes.cs +++ b/src/Vogen/BuildConfigurationFromAttributes.cs @@ -198,13 +198,6 @@ private void PopulateDiagnosticsWithAnyValidationIssues() _diagnostics.Add(DiagnosticsCatalogue.InvalidDeserializationStrictness(syntaxLocation)); } - bool hasActiveDefaultComparison = _stringDefaultComparison is not StringComparisonDefault.Unspecified - and not StringComparisonDefault.Omit; - - if (hasActiveDefaultComparison && _stringComparers == StringComparersGeneration.Omit) - { - _diagnostics.Add(DiagnosticsCatalogue.StringDefaultComparisonConflictsWithStringComparersOmit(syntaxLocation)); - } } // populates all args - it doesn't expect the underlying type argument as that is: diff --git a/src/Vogen/Diagnostics/DiagnosticsCatalogue.cs b/src/Vogen/Diagnostics/DiagnosticsCatalogue.cs index 1082286d589..6d1db423dd6 100644 --- a/src/Vogen/Diagnostics/DiagnosticsCatalogue.cs +++ b/src/Vogen/Diagnostics/DiagnosticsCatalogue.cs @@ -141,11 +141,6 @@ internal static class DiagnosticsCatalogue "'{0}' has StringDefaultComparison set, but the underlying type '{1}' is not a string. StringDefaultComparison only applies to string-backed value objects.", DiagnosticSeverity.Warning); - private static readonly DiagnosticDescriptor _stringDefaultComparisonConflictsWithStringComparersOmit = CreateDescriptor( - RuleIdentifiers.StringDefaultComparisonConflictsWithStringComparersOmit, - "StringDefaultComparison conflicts with StringComparersGeneration.Omit", - "Cannot set stringComparers to Omit when stringDefaultComparison is specified, as the Comparers class must be generated to support the default comparison."); - public static Diagnostic TypeCannotBeNested(INamedTypeSymbol typeModel, INamedTypeSymbol container) => Create(_typeCannotBeNested, typeModel.Locations, typeModel.Name, container.Name); @@ -228,9 +223,6 @@ public static Diagnostic NumericsGenerationNotApplicable(INamedTypeSymbol voSymb public static Diagnostic StringDefaultComparisonNotApplicable(INamedTypeSymbol voSymbol, ITypeSymbol underlyingType) => Create(_stringDefaultComparisonNotApplicable, voSymbol.Locations, voSymbol.Name, underlyingType.Name); - public static Diagnostic StringDefaultComparisonConflictsWithStringComparersOmit(Location location) => - Diagnostic.Create(_stringDefaultComparisonConflictsWithStringComparersOmit, location); - private static DiagnosticDescriptor CreateDescriptor(string code, string title, string messageFormat, DiagnosticSeverity severity = DiagnosticSeverity.Error) { string[] tags = severity == DiagnosticSeverity.Error ? new[] { WellKnownDiagnosticTags.NotConfigurable } : Array.Empty(); diff --git a/src/Vogen/Diagnostics/RuleIdentifiers.cs b/src/Vogen/Diagnostics/RuleIdentifiers.cs index b5a1604ee32..d4b5bb8687c 100644 --- a/src/Vogen/Diagnostics/RuleIdentifiers.cs +++ b/src/Vogen/Diagnostics/RuleIdentifiers.cs @@ -46,5 +46,4 @@ public static class RuleIdentifiers public const string NumericsGenerationNotApplicable = "VOG037"; public const string PropertyOfValueObjectShouldBeInitialized = "VOG038"; public const string StringDefaultComparisonNotApplicable = "VOG039"; - public const string StringDefaultComparisonConflictsWithStringComparersOmit = "VOG040"; } \ No newline at end of file diff --git a/src/Vogen/GenerateCodeForStringComparers.cs b/src/Vogen/GenerateCodeForStringComparers.cs index e6f56c9f9df..52efe9558cc 100644 --- a/src/Vogen/GenerateCodeForStringComparers.cs +++ b/src/Vogen/GenerateCodeForStringComparers.cs @@ -11,10 +11,7 @@ public static string GenerateIfNeeded(VoWorkItem item, TypeDeclarationSyntax tds return string.Empty; } - bool hasDefaultComparison = item.Config.StringDefaultComparison is not StringComparisonDefault.Unspecified - and not StringComparisonDefault.Omit; - - if (item.Config.StringComparers != StringComparersGeneration.Generate && !hasDefaultComparison) + if (item.Config.StringComparers != StringComparersGeneration.Generate) { return string.Empty; } diff --git a/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs b/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs index 9640dd645da..f199c11a0dd 100644 --- a/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs +++ b/tests/SnapshotTests/StringComparison/StringComparisonGenerationTests.cs @@ -56,6 +56,23 @@ public partial class StringThing { } .RunOnAllFrameworks(); } + [Fact] + public Task Generates_comparers_class_with_default_comparison_when_both_are_specified() + { + string source = $$""" + using System; + using Vogen; + namespace Whatever; + + [ValueObject(typeof(string), Conversions.Default, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Generate, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] + public partial class StringThing { } + """; + + return new SnapshotRunner() + .WithSource(source) + .RunOnAllFrameworks(); + } + [Fact] public Task Does_not_generate_the_equals_method_that_takes_a_StringComparison_when_not_a_string() { diff --git a/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt b/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt new file mode 100644 index 00000000000..62b4cd9b562 --- /dev/null +++ b/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt @@ -0,0 +1,620 @@ +[ +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace generator +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + public class VogenTypesFactory : global::System.Text.Json.Serialization.JsonConverterFactory + { + public VogenTypesFactory() + { + } + + private static readonly global::System.Collections.Generic.Dictionary> _lookup = new global::System.Collections.Generic.Dictionary> + { + { + typeof(global::Whatever.StringThing), + new global::System.Lazy(() => new global::Whatever.StringThing.StringThingSystemTextJsonConverter()) + } + }; + public override bool CanConvert(global::System.Type typeToConvert) => _lookup.ContainsKey(typeToConvert); + public override global::System.Text.Json.Serialization.JsonConverter CreateConverter(global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) => _lookup[typeToConvert].Value; + } +} + +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace Whatever +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + [global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] + [global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] + [global::System.Diagnostics.DebuggerTypeProxyAttribute(typeof(StringThingDebugView))] + [global::System.Diagnostics.DebuggerDisplayAttribute("Underlying type: global::System.String, Value = { _value }")] + public partial class StringThing : global::System.IEquatable, global::System.IEquatable, global::System.IComparable, global::System.IComparable, global::System.IParsable, global::System.IConvertible + { +#if DEBUG +private readonly global::System.Diagnostics.StackTrace _stackTrace = null!; +#endif +#if !VOGEN_NO_VALIDATION + private readonly global::System.Boolean _isInitialized; +#endif + private readonly global::System.String _value; + /// + /// Gets the underlying value if set, otherwise a is thrown. + /// + public global::System.String Value + { + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get + { + EnsureInitialized(); + return _value; + } + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public StringThing() + { +#if DEBUG + _stackTrace = new global::System.Diagnostics.StackTrace(); +#endif +#if !VOGEN_NO_VALIDATION + _isInitialized = false; +#endif + _value = default; + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private StringThing(global::System.String value) + { + _value = value; +#if !VOGEN_NO_VALIDATION + _isInitialized = true; +#endif + } + + /// + /// Builds an instance from the provided underlying type. + /// + /// The underlying type. + /// An instance of this type. + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static StringThing From(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + /// + /// Tries to build an instance from the provided underlying type. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, false will be returned. + /// + /// The underlying type. + /// An instance of the value object. + /// True if the value object can be built, otherwise false. + public static bool TryFrom( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String value, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing vo) + { + if (value is null) + { + vo = default; + return false; + } + + vo = new StringThing(value); + return true; + } + + /// + /// Tries to build an instance from the provided underlying value. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, an error will be returned. + /// + /// The primitive value. + /// A containing either the value object, or an error. + public static global::Vogen.ValueObjectOrError TryFrom(global::System.String value) + { + if (value is null) + { + return new global::Vogen.ValueObjectOrError(global::Vogen.Validation.Invalid("The value provided was null")); + } + + return new global::Vogen.ValueObjectOrError(new StringThing(value)); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] +#if VOGEN_NO_VALIDATION +#pragma warning disable CS8775 + public bool IsInitialized() => true; +#pragma warning restore CS8775 +#else + public bool IsInitialized() => _isInitialized; +#endif +#nullable disable + public static class Comparers + { + private class __StringEqualityComparer : global::System.Collections.Generic.IEqualityComparer + { + readonly global::System.StringComparer _comparer; + public __StringEqualityComparer(global::System.StringComparer comparer) + { + _comparer = comparer; + } + + public bool Equals(StringThing x, StringThing y) + { + return _comparer.Equals(x._value, y._value); + } + + public int GetHashCode(StringThing obj) + { + return _comparer.GetHashCode(obj._value); + } + } + + public static global::System.Collections.Generic.IEqualityComparer Ordinal => new __StringEqualityComparer(global::System.StringComparer.Ordinal); + public static global::System.Collections.Generic.IEqualityComparer OrdinalIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.OrdinalIgnoreCase); + public static global::System.Collections.Generic.IEqualityComparer CurrentCulture => new __StringEqualityComparer(global::System.StringComparer.CurrentCulture); + public static global::System.Collections.Generic.IEqualityComparer CurrentCultureIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.CurrentCultureIgnoreCase); + public static global::System.Collections.Generic.IEqualityComparer InvariantCulture => new __StringEqualityComparer(global::System.StringComparer.InvariantCulture); + public static global::System.Collections.Generic.IEqualityComparer InvariantCultureIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.InvariantCultureIgnoreCase); +#nullable restore + } + + // only called internally when something has been deserialized into + // its primitive type. + private static StringThing __Deserialize(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + public global::System.Boolean Equals(StringThing other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + // It's possible to create uninitialized instances via converters such as EfCore (HasDefaultValue), which call Equals. + // We treat anything uninitialized as not equal to anything, even other uninitialized instances of this type. + if (!IsInitialized() || !other.IsInitialized()) + return false; + if (ReferenceEquals(this, other)) + { + return true; + } + + return GetType() == other.GetType() && global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, other.Value); + } + + public global::System.Boolean Equals(StringThing other, global::System.Collections.Generic.IEqualityComparer comparer) + { + return comparer.Equals(this, other); + } + + public global::System.Boolean Equals(global::System.String primitive) + { + return global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, primitive); + } + + public global::System.Boolean Equals(global::System.String primitive, global::System.StringComparer comparer) + { + return comparer.Equals(Value, primitive); + } + + public override global::System.Boolean Equals(global::System.Object obj) + { + return Equals(obj as StringThing); + } + + public static global::System.Boolean operator ==(StringThing left, StringThing right) => Equals(left, right); + public static global::System.Boolean operator !=(StringThing left, StringThing right) => !Equals(left, right); + public static global::System.Boolean operator ==(StringThing left, global::System.String right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left.Value, right); + public static global::System.Boolean operator ==(global::System.String left, StringThing right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left, right.Value); + public static global::System.Boolean operator !=(global::System.String left, StringThing right) => !(left == right); + public static global::System.Boolean operator !=(StringThing left, global::System.String right) => !(left == right); + public static explicit operator StringThing(global::System.String value) => From(value); + public static explicit operator global::System.String(StringThing value) => value.Value; + public int CompareTo(StringThing other) + { + if (other is null) + return 1; + return Value.CompareTo(other.Value); + } + + public int CompareTo(object other) + { + if (other is null) + return 1; + if (other is StringThing x) + return CompareTo(x); + ThrowHelper.ThrowArgumentException("Cannot compare to object as it is not of type StringThing", nameof(other)); + return 0; + } + + /// + /// + /// + /// True if the value passes any validation (after running any optional normalization). + /// + public static global::System.Boolean TryParse( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String s, global::System.IFormatProvider provider, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing result) + { + if (s is null) + { + result = default; + return false; + } + + result = new StringThing(s); + return true; + } + + /// + /// + /// + /// The value created via the method. + /// + /// Thrown when the value can be parsed, but is not valid. + public static StringThing Parse(global::System.String s, global::System.IFormatProvider provider) + { + return From(s); + } + +#nullable disable +#nullable restore +#nullable disable + /// + public System.TypeCode GetTypeCode() + { + return IsInitialized() ? Value.GetTypeCode() : default; + } + + /// + bool System.IConvertible.ToBoolean(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToBoolean(provider) : default; + } + + /// + byte System.IConvertible.ToByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToByte(provider) : default; + } + + /// + char System.IConvertible.ToChar(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToChar(provider) : default; + } + + /// + System.DateTime System.IConvertible.ToDateTime(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDateTime(provider) : default; + } + + /// + decimal System.IConvertible.ToDecimal(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDecimal(provider) : default; + } + + /// + double System.IConvertible.ToDouble(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDouble(provider) : default; + } + + /// + short System.IConvertible.ToInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt16(provider) : default; + } + + /// + int System.IConvertible.ToInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt32(provider) : default; + } + + /// + long System.IConvertible.ToInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt64(provider) : default; + } + + /// + sbyte System.IConvertible.ToSByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSByte(provider) : default; + } + + /// + float System.IConvertible.ToSingle(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSingle(provider) : default; + } + + /// + object System.IConvertible.ToType(global::System.Type @type, global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToType(@type, provider) : default; + } + + /// + ushort System.IConvertible.ToUInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt16(provider) : default; + } + + /// + uint System.IConvertible.ToUInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt32(provider) : default; + } + + /// + ulong System.IConvertible.ToUInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt64(provider) : default; + } + +#nullable restore + public override global::System.Int32 GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + global::System.Int32 hash = (global::System.Int32)2166136261; + hash = (hash * 16777619) ^ GetType().GetHashCode(); + hash = (hash * 16777619) ^ global::System.StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + return hash; + } + } + + /// + public override global::System.String ToString() => IsInitialized() ? Value.ToString() ?? "" : "[UNINITIALIZED]"; + /// + public global::System.String ToString(global::System.IFormatProvider provider) => IsInitialized() ? Value.ToString(provider) ?? "" : "[UNINITIALIZED]"; + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private void EnsureInitialized() + { + if (!IsInitialized()) + { +#if DEBUG + ThrowHelper.ThrowWhenNotInitialized(_stackTrace); +#else + ThrowHelper.ThrowWhenNotInitialized(); +#endif + } + } + +#nullable disable + /// + /// Converts a StringThing to or from JSON. + /// + public partial class StringThingSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override StringThing Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + +#if NET6_0_OR_GREATER + public override StringThing ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WritePropertyName(value.Value); + } +#endif +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + private static void ThrowJsonExceptionWhenValidationFails(global::Vogen.Validation validation) + { + var e = ThrowHelper.CreateValidationException(validation); + throw new global::System.Text.Json.JsonException(null, e); + } + + private static void ThrowJsonExceptionWhenNull(global::System.String value) + { + if (value == null) + { + var e = ThrowHelper.CreateCannotBeNullException(); + throw new global::System.Text.Json.JsonException(null, e); + } + } + + private static StringThing DeserializeJson(global::System.String value) + { + ThrowJsonExceptionWhenNull(value); + return new StringThing(value); + } + } + +#nullable restore +#nullable disable + class StringThingTypeConverter : global::System.ComponentModel.TypeConverter + { + public override global::System.Boolean CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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) + { + var stringValue = value as global::System.String; + if (stringValue != null) + { + return StringThing.__Deserialize(stringValue); + } + + return base.ConvertFrom(context, culture, value); + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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 StringThing idValue) + { + if (destinationType == typeof(global::System.String)) + { + return idValue.Value; + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + +#nullable restore + internal sealed class StringThingDebugView + { + private readonly StringThing _t; + StringThingDebugView(StringThing t) + { + _t = t; + } + + public global::System.String UnderlyingType => "global::System.String"; + public global::System.String Value => _t.Value; + public global::System.String Conversions => @"[global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] +[global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] +"; + } + + static class ThrowHelper + { +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowInvalidOperationException(string message) => throw new global::System.InvalidOperationException(message); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowArgumentException(string message, string arg) => throw new global::System.ArgumentException(message, arg); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenCreatedWithNull() => throw CreateCannotBeNullException(); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized() => throw CreateValidationException("Use of uninitialized Value Object."); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized(global::System.Diagnostics.StackTrace stackTrace) => throw CreateValidationException("Use of uninitialized Value Object at: " + stackTrace ?? ""); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenValidationFails(global::Vogen.Validation validation) + { + throw CreateValidationException(validation); + } + + internal static global::System.Exception CreateValidationException(string message) => new global::Vogen.ValueObjectValidationException(message); + internal static global::System.Exception CreateCannotBeNullException() => new global::Vogen.ValueObjectValidationException("Cannot create a value object with null."); + internal static global::System.Exception CreateValidationException(global::Vogen.Validation validation) + { + var ex = CreateValidationException(validation.ErrorMessage); + if (validation.Data != null) + { + foreach (var kvp in validation.Data) + { + ex.Data[kvp.Key] = kvp.Value; + } + } + + return ex; + } + } + } +} +] \ No newline at end of file diff --git a/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt b/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt index 62b4cd9b562..461a0c4ec01 100644 --- a/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt +++ b/tests/SnapshotTests/StringComparison/snapshots/snap-v9.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt @@ -190,37 +190,6 @@ private readonly global::System.Diagnostics.StackTrace _stackTrace = null!; #else public bool IsInitialized() => _isInitialized; #endif -#nullable disable - public static class Comparers - { - private class __StringEqualityComparer : global::System.Collections.Generic.IEqualityComparer - { - readonly global::System.StringComparer _comparer; - public __StringEqualityComparer(global::System.StringComparer comparer) - { - _comparer = comparer; - } - - public bool Equals(StringThing x, StringThing y) - { - return _comparer.Equals(x._value, y._value); - } - - public int GetHashCode(StringThing obj) - { - return _comparer.GetHashCode(obj._value); - } - } - - public static global::System.Collections.Generic.IEqualityComparer Ordinal => new __StringEqualityComparer(global::System.StringComparer.Ordinal); - public static global::System.Collections.Generic.IEqualityComparer OrdinalIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.OrdinalIgnoreCase); - public static global::System.Collections.Generic.IEqualityComparer CurrentCulture => new __StringEqualityComparer(global::System.StringComparer.CurrentCulture); - public static global::System.Collections.Generic.IEqualityComparer CurrentCultureIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.CurrentCultureIgnoreCase); - public static global::System.Collections.Generic.IEqualityComparer InvariantCulture => new __StringEqualityComparer(global::System.StringComparer.InvariantCulture); - public static global::System.Collections.Generic.IEqualityComparer InvariantCultureIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.InvariantCultureIgnoreCase); -#nullable restore - } - // only called internally when something has been deserialized into // its primitive type. private static StringThing __Deserialize(global::System.String value) From 07a9a0e6b0b2fac9630bb5493e8327e9713dbe34 Mon Sep 17 00:00:00 2001 From: Sir Phillip Tubell Date: Thu, 18 Jun 2026 10:12:20 -0400 Subject: [PATCH 4/5] fix: use Conversions.Default in string comparison consumer test types Conversions.Unspecified = -1 fails the IsValidFlags() >= 0 check, causing VOG011 on the four string default comparison test types. --- tests/ConsumerTests/StringComparisons/Types.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ConsumerTests/StringComparisons/Types.cs b/tests/ConsumerTests/StringComparisons/Types.cs index 54c664d2f23..717ee9feafa 100644 --- a/tests/ConsumerTests/StringComparisons/Types.cs +++ b/tests/ConsumerTests/StringComparisons/Types.cs @@ -91,22 +91,22 @@ public partial struct StringVo_Struct_Generic { } -[ValueObject(typeof(string), Conversions.Unspecified, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] +[ValueObject(typeof(string), Conversions.Default, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] public partial class StringVo_Class_DefaultOrdinalIgnoreCase { } -[ValueObject(typeof(string), Conversions.Unspecified, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] +[ValueObject(typeof(string), Conversions.Default, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] public partial record class StringVo_RecordClass_DefaultOrdinalIgnoreCase { } -[ValueObject(typeof(string), Conversions.Unspecified, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] +[ValueObject(typeof(string), Conversions.Default, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] public partial record struct StringVo_RecordStruct_DefaultOrdinalIgnoreCase { } -[ValueObject(typeof(string), Conversions.Unspecified, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] +[ValueObject(typeof(string), Conversions.Default, null, Customizations.None, DeserializationStrictness.AllowValidAndKnownInstances, DebuggerAttributeGeneration.Default, ComparisonGeneration.Default, StringComparersGeneration.Unspecified, CastOperator.Unspecified, CastOperator.Unspecified, ParsableForStrings.Unspecified, ParsableForPrimitives.Unspecified, TryFromGeneration.Unspecified, IsInitializedMethodGeneration.Unspecified, PrimitiveEqualityGeneration.Unspecified, NumericsGeneration.Unspecified, StringComparisonDefault.OrdinalIgnoreCase)] public partial struct StringVo_Struct_DefaultOrdinalIgnoreCase { } From ef5aa71b4ac1a94f87aea64cd3a298d4a4b5691f Mon Sep 17 00:00:00 2001 From: Sir Phillip Tubell Date: Thu, 18 Jun 2026 10:38:43 -0400 Subject: [PATCH 5/5] fix: add missing v4.8 and v8.0 snapshots for string comparison tests Thorough CI builds run all three frameworks; the OrdinalIgnoreCase and combined-settings tests were missing their v4.8 and v8.0 verified snapshots. --- ...rison_when_both_are_specified.verified.txt | 620 ++++++++++++++++++ ...ests.Generates_when_specified.verified.txt | 2 +- ..._comparison_OrdinalIgnoreCase.verified.txt | 589 +++++++++++++++++ ...rison_when_both_are_specified.verified.txt | 620 ++++++++++++++++++ ...ests.Generates_when_specified.verified.txt | 2 +- ..._comparison_OrdinalIgnoreCase.verified.txt | 589 +++++++++++++++++ 6 files changed, 2420 insertions(+), 2 deletions(-) create mode 100644 tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt create mode 100644 tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt create mode 100644 tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt create mode 100644 tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt diff --git a/tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt b/tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt new file mode 100644 index 00000000000..cea860f8bfe --- /dev/null +++ b/tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt @@ -0,0 +1,620 @@ +[ +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace generator +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + public class VogenTypesFactory : global::System.Text.Json.Serialization.JsonConverterFactory + { + public VogenTypesFactory() + { + } + + private static readonly global::System.Collections.Generic.Dictionary> _lookup = new global::System.Collections.Generic.Dictionary> + { + { + typeof(global::Whatever.StringThing), + new global::System.Lazy(() => new global::Whatever.StringThing.StringThingSystemTextJsonConverter()) + } + }; + public override bool CanConvert(global::System.Type typeToConvert) => _lookup.ContainsKey(typeToConvert); + public override global::System.Text.Json.Serialization.JsonConverter CreateConverter(global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) => _lookup[typeToConvert].Value; + } +} + +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace Whatever +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + [global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] + [global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] + [global::System.Diagnostics.DebuggerTypeProxyAttribute(typeof(StringThingDebugView))] + [global::System.Diagnostics.DebuggerDisplayAttribute("Underlying type: global::System.String, Value = { _value }")] + public partial class StringThing : global::System.IEquatable, global::System.IEquatable, global::System.IComparable, global::System.IComparable, global::System.IConvertible + { +#if DEBUG +private readonly global::System.Diagnostics.StackTrace _stackTrace = null!; +#endif +#if !VOGEN_NO_VALIDATION + private readonly global::System.Boolean _isInitialized; +#endif + private readonly global::System.String _value; + /// + /// Gets the underlying value if set, otherwise a is thrown. + /// + public global::System.String Value + { + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get + { + EnsureInitialized(); + return _value; + } + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public StringThing() + { +#if DEBUG + _stackTrace = new global::System.Diagnostics.StackTrace(); +#endif +#if !VOGEN_NO_VALIDATION + _isInitialized = false; +#endif + _value = default; + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private StringThing(global::System.String value) + { + _value = value; +#if !VOGEN_NO_VALIDATION + _isInitialized = true; +#endif + } + + /// + /// Builds an instance from the provided underlying type. + /// + /// The underlying type. + /// An instance of this type. + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static StringThing From(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + /// + /// Tries to build an instance from the provided underlying type. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, false will be returned. + /// + /// The underlying type. + /// An instance of the value object. + /// True if the value object can be built, otherwise false. + public static bool TryFrom( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String value, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing vo) + { + if (value is null) + { + vo = default; + return false; + } + + vo = new StringThing(value); + return true; + } + + /// + /// Tries to build an instance from the provided underlying value. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, an error will be returned. + /// + /// The primitive value. + /// A containing either the value object, or an error. + public static global::Vogen.ValueObjectOrError TryFrom(global::System.String value) + { + if (value is null) + { + return new global::Vogen.ValueObjectOrError(global::Vogen.Validation.Invalid("The value provided was null")); + } + + return new global::Vogen.ValueObjectOrError(new StringThing(value)); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] +#if VOGEN_NO_VALIDATION +#pragma warning disable CS8775 + public bool IsInitialized() => true; +#pragma warning restore CS8775 +#else + public bool IsInitialized() => _isInitialized; +#endif +#nullable disable + public static class Comparers + { + private class __StringEqualityComparer : global::System.Collections.Generic.IEqualityComparer + { + readonly global::System.StringComparer _comparer; + public __StringEqualityComparer(global::System.StringComparer comparer) + { + _comparer = comparer; + } + + public bool Equals(StringThing x, StringThing y) + { + return _comparer.Equals(x._value, y._value); + } + + public int GetHashCode(StringThing obj) + { + return _comparer.GetHashCode(obj._value); + } + } + + public static global::System.Collections.Generic.IEqualityComparer Ordinal => new __StringEqualityComparer(global::System.StringComparer.Ordinal); + public static global::System.Collections.Generic.IEqualityComparer OrdinalIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.OrdinalIgnoreCase); + public static global::System.Collections.Generic.IEqualityComparer CurrentCulture => new __StringEqualityComparer(global::System.StringComparer.CurrentCulture); + public static global::System.Collections.Generic.IEqualityComparer CurrentCultureIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.CurrentCultureIgnoreCase); + public static global::System.Collections.Generic.IEqualityComparer InvariantCulture => new __StringEqualityComparer(global::System.StringComparer.InvariantCulture); + public static global::System.Collections.Generic.IEqualityComparer InvariantCultureIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.InvariantCultureIgnoreCase); +#nullable restore + } + + // only called internally when something has been deserialized into + // its primitive type. + private static StringThing __Deserialize(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + public global::System.Boolean Equals(StringThing other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + // It's possible to create uninitialized instances via converters such as EfCore (HasDefaultValue), which call Equals. + // We treat anything uninitialized as not equal to anything, even other uninitialized instances of this type. + if (!IsInitialized() || !other.IsInitialized()) + return false; + if (ReferenceEquals(this, other)) + { + return true; + } + + return GetType() == other.GetType() && global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, other.Value); + } + + public global::System.Boolean Equals(StringThing other, global::System.Collections.Generic.IEqualityComparer comparer) + { + return comparer.Equals(this, other); + } + + public global::System.Boolean Equals(global::System.String primitive) + { + return global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, primitive); + } + + public global::System.Boolean Equals(global::System.String primitive, global::System.StringComparer comparer) + { + return comparer.Equals(Value, primitive); + } + + public override global::System.Boolean Equals(global::System.Object obj) + { + return Equals(obj as StringThing); + } + + public static global::System.Boolean operator ==(StringThing left, StringThing right) => Equals(left, right); + public static global::System.Boolean operator !=(StringThing left, StringThing right) => !Equals(left, right); + public static global::System.Boolean operator ==(StringThing left, global::System.String right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left.Value, right); + public static global::System.Boolean operator ==(global::System.String left, StringThing right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left, right.Value); + public static global::System.Boolean operator !=(global::System.String left, StringThing right) => !(left == right); + public static global::System.Boolean operator !=(StringThing left, global::System.String right) => !(left == right); + public static explicit operator StringThing(global::System.String value) => From(value); + public static explicit operator global::System.String(StringThing value) => value.Value; + public int CompareTo(StringThing other) + { + if (other is null) + return 1; + return Value.CompareTo(other.Value); + } + + public int CompareTo(object other) + { + if (other is null) + return 1; + if (other is StringThing x) + return CompareTo(x); + ThrowHelper.ThrowArgumentException("Cannot compare to object as it is not of type StringThing", nameof(other)); + return 0; + } + + /// + /// + /// + /// True if the value passes any validation (after running any optional normalization). + /// + public static global::System.Boolean TryParse( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String s, global::System.IFormatProvider provider, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing result) + { + if (s is null) + { + result = default; + return false; + } + + result = new StringThing(s); + return true; + } + + /// + /// + /// + /// The value created via the method. + /// + /// Thrown when the value can be parsed, but is not valid. + public static StringThing Parse(global::System.String s, global::System.IFormatProvider provider) + { + return From(s); + } + +#nullable disable +#nullable restore +#nullable disable + /// + public System.TypeCode GetTypeCode() + { + return IsInitialized() ? Value.GetTypeCode() : default; + } + + /// + bool System.IConvertible.ToBoolean(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToBoolean(provider) : default; + } + + /// + char System.IConvertible.ToChar(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToChar(provider) : default; + } + + /// + sbyte System.IConvertible.ToSByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSByte(provider) : default; + } + + /// + byte System.IConvertible.ToByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToByte(provider) : default; + } + + /// + short System.IConvertible.ToInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt16(provider) : default; + } + + /// + ushort System.IConvertible.ToUInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt16(provider) : default; + } + + /// + int System.IConvertible.ToInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt32(provider) : default; + } + + /// + uint System.IConvertible.ToUInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt32(provider) : default; + } + + /// + long System.IConvertible.ToInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt64(provider) : default; + } + + /// + ulong System.IConvertible.ToUInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt64(provider) : default; + } + + /// + float System.IConvertible.ToSingle(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSingle(provider) : default; + } + + /// + double System.IConvertible.ToDouble(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDouble(provider) : default; + } + + /// + decimal System.IConvertible.ToDecimal(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDecimal(provider) : default; + } + + /// + System.DateTime System.IConvertible.ToDateTime(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDateTime(provider) : default; + } + + /// + object System.IConvertible.ToType(global::System.Type @type, global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToType(@type, provider) : default; + } + +#nullable restore + public override global::System.Int32 GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + global::System.Int32 hash = (global::System.Int32)2166136261; + hash = (hash * 16777619) ^ GetType().GetHashCode(); + hash = (hash * 16777619) ^ global::System.StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + return hash; + } + } + + /// + public override global::System.String ToString() => IsInitialized() ? Value.ToString() ?? "" : "[UNINITIALIZED]"; + /// + public global::System.String ToString(global::System.IFormatProvider provider) => IsInitialized() ? Value.ToString(provider) ?? "" : "[UNINITIALIZED]"; + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private void EnsureInitialized() + { + if (!IsInitialized()) + { +#if DEBUG + ThrowHelper.ThrowWhenNotInitialized(_stackTrace); +#else + ThrowHelper.ThrowWhenNotInitialized(); +#endif + } + } + +#nullable disable + /// + /// Converts a StringThing to or from JSON. + /// + public partial class StringThingSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override StringThing Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + +#if NET6_0_OR_GREATER + public override StringThing ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WritePropertyName(value.Value); + } +#endif +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + private static void ThrowJsonExceptionWhenValidationFails(global::Vogen.Validation validation) + { + var e = ThrowHelper.CreateValidationException(validation); + throw new global::System.Text.Json.JsonException(null, e); + } + + private static void ThrowJsonExceptionWhenNull(global::System.String value) + { + if (value == null) + { + var e = ThrowHelper.CreateCannotBeNullException(); + throw new global::System.Text.Json.JsonException(null, e); + } + } + + private static StringThing DeserializeJson(global::System.String value) + { + ThrowJsonExceptionWhenNull(value); + return new StringThing(value); + } + } + +#nullable restore +#nullable disable + class StringThingTypeConverter : global::System.ComponentModel.TypeConverter + { + public override global::System.Boolean CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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) + { + var stringValue = value as global::System.String; + if (stringValue != null) + { + return StringThing.__Deserialize(stringValue); + } + + return base.ConvertFrom(context, culture, value); + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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 StringThing idValue) + { + if (destinationType == typeof(global::System.String)) + { + return idValue.Value; + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + +#nullable restore + internal sealed class StringThingDebugView + { + private readonly StringThing _t; + StringThingDebugView(StringThing t) + { + _t = t; + } + + public global::System.String UnderlyingType => "global::System.String"; + public global::System.String Value => _t.Value; + public global::System.String Conversions => @"[global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] +[global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] +"; + } + + static class ThrowHelper + { +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowInvalidOperationException(string message) => throw new global::System.InvalidOperationException(message); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowArgumentException(string message, string arg) => throw new global::System.ArgumentException(message, arg); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenCreatedWithNull() => throw CreateCannotBeNullException(); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized() => throw CreateValidationException("Use of uninitialized Value Object."); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized(global::System.Diagnostics.StackTrace stackTrace) => throw CreateValidationException("Use of uninitialized Value Object at: " + stackTrace ?? ""); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenValidationFails(global::Vogen.Validation validation) + { + throw CreateValidationException(validation); + } + + internal static global::System.Exception CreateValidationException(string message) => new global::Vogen.ValueObjectValidationException(message); + internal static global::System.Exception CreateCannotBeNullException() => new global::Vogen.ValueObjectValidationException("Cannot create a value object with null."); + internal static global::System.Exception CreateValidationException(global::Vogen.Validation validation) + { + var ex = CreateValidationException(validation.ErrorMessage); + if (validation.Data != null) + { + foreach (var kvp in validation.Data) + { + ex.Data[kvp.Key] = kvp.Value; + } + } + + return ex; + } + } + } +} +] \ No newline at end of file diff --git a/tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_when_specified.verified.txt b/tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_when_specified.verified.txt index 7c9943c0d1a..5662b7abb80 100644 --- a/tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_when_specified.verified.txt +++ b/tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_when_specified.verified.txt @@ -208,7 +208,7 @@ private readonly global::System.Diagnostics.StackTrace _stackTrace = null!; public int GetHashCode(StringThing obj) { - return _comparer.GetHashCode(); + return _comparer.GetHashCode(obj._value); } } diff --git a/tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt b/tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt new file mode 100644 index 00000000000..661b6febf13 --- /dev/null +++ b/tests/SnapshotTests/StringComparison/snapshots/snap-v4.8/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt @@ -0,0 +1,589 @@ +[ +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace generator +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + public class VogenTypesFactory : global::System.Text.Json.Serialization.JsonConverterFactory + { + public VogenTypesFactory() + { + } + + private static readonly global::System.Collections.Generic.Dictionary> _lookup = new global::System.Collections.Generic.Dictionary> + { + { + typeof(global::Whatever.StringThing), + new global::System.Lazy(() => new global::Whatever.StringThing.StringThingSystemTextJsonConverter()) + } + }; + public override bool CanConvert(global::System.Type typeToConvert) => _lookup.ContainsKey(typeToConvert); + public override global::System.Text.Json.Serialization.JsonConverter CreateConverter(global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) => _lookup[typeToConvert].Value; + } +} + +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace Whatever +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + [global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] + [global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] + [global::System.Diagnostics.DebuggerTypeProxyAttribute(typeof(StringThingDebugView))] + [global::System.Diagnostics.DebuggerDisplayAttribute("Underlying type: global::System.String, Value = { _value }")] + public partial class StringThing : global::System.IEquatable, global::System.IEquatable, global::System.IComparable, global::System.IComparable, global::System.IConvertible + { +#if DEBUG +private readonly global::System.Diagnostics.StackTrace _stackTrace = null!; +#endif +#if !VOGEN_NO_VALIDATION + private readonly global::System.Boolean _isInitialized; +#endif + private readonly global::System.String _value; + /// + /// Gets the underlying value if set, otherwise a is thrown. + /// + public global::System.String Value + { + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get + { + EnsureInitialized(); + return _value; + } + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public StringThing() + { +#if DEBUG + _stackTrace = new global::System.Diagnostics.StackTrace(); +#endif +#if !VOGEN_NO_VALIDATION + _isInitialized = false; +#endif + _value = default; + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private StringThing(global::System.String value) + { + _value = value; +#if !VOGEN_NO_VALIDATION + _isInitialized = true; +#endif + } + + /// + /// Builds an instance from the provided underlying type. + /// + /// The underlying type. + /// An instance of this type. + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static StringThing From(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + /// + /// Tries to build an instance from the provided underlying type. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, false will be returned. + /// + /// The underlying type. + /// An instance of the value object. + /// True if the value object can be built, otherwise false. + public static bool TryFrom( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String value, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing vo) + { + if (value is null) + { + vo = default; + return false; + } + + vo = new StringThing(value); + return true; + } + + /// + /// Tries to build an instance from the provided underlying value. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, an error will be returned. + /// + /// The primitive value. + /// A containing either the value object, or an error. + public static global::Vogen.ValueObjectOrError TryFrom(global::System.String value) + { + if (value is null) + { + return new global::Vogen.ValueObjectOrError(global::Vogen.Validation.Invalid("The value provided was null")); + } + + return new global::Vogen.ValueObjectOrError(new StringThing(value)); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] +#if VOGEN_NO_VALIDATION +#pragma warning disable CS8775 + public bool IsInitialized() => true; +#pragma warning restore CS8775 +#else + public bool IsInitialized() => _isInitialized; +#endif + // only called internally when something has been deserialized into + // its primitive type. + private static StringThing __Deserialize(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + public global::System.Boolean Equals(StringThing other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + // It's possible to create uninitialized instances via converters such as EfCore (HasDefaultValue), which call Equals. + // We treat anything uninitialized as not equal to anything, even other uninitialized instances of this type. + if (!IsInitialized() || !other.IsInitialized()) + return false; + if (ReferenceEquals(this, other)) + { + return true; + } + + return GetType() == other.GetType() && global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, other.Value); + } + + public global::System.Boolean Equals(StringThing other, global::System.Collections.Generic.IEqualityComparer comparer) + { + return comparer.Equals(this, other); + } + + public global::System.Boolean Equals(global::System.String primitive) + { + return global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, primitive); + } + + public global::System.Boolean Equals(global::System.String primitive, global::System.StringComparer comparer) + { + return comparer.Equals(Value, primitive); + } + + public override global::System.Boolean Equals(global::System.Object obj) + { + return Equals(obj as StringThing); + } + + public static global::System.Boolean operator ==(StringThing left, StringThing right) => Equals(left, right); + public static global::System.Boolean operator !=(StringThing left, StringThing right) => !Equals(left, right); + public static global::System.Boolean operator ==(StringThing left, global::System.String right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left.Value, right); + public static global::System.Boolean operator ==(global::System.String left, StringThing right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left, right.Value); + public static global::System.Boolean operator !=(global::System.String left, StringThing right) => !(left == right); + public static global::System.Boolean operator !=(StringThing left, global::System.String right) => !(left == right); + public static explicit operator StringThing(global::System.String value) => From(value); + public static explicit operator global::System.String(StringThing value) => value.Value; + public int CompareTo(StringThing other) + { + if (other is null) + return 1; + return Value.CompareTo(other.Value); + } + + public int CompareTo(object other) + { + if (other is null) + return 1; + if (other is StringThing x) + return CompareTo(x); + ThrowHelper.ThrowArgumentException("Cannot compare to object as it is not of type StringThing", nameof(other)); + return 0; + } + + /// + /// + /// + /// True if the value passes any validation (after running any optional normalization). + /// + public static global::System.Boolean TryParse( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String s, global::System.IFormatProvider provider, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing result) + { + if (s is null) + { + result = default; + return false; + } + + result = new StringThing(s); + return true; + } + + /// + /// + /// + /// The value created via the method. + /// + /// Thrown when the value can be parsed, but is not valid. + public static StringThing Parse(global::System.String s, global::System.IFormatProvider provider) + { + return From(s); + } + +#nullable disable +#nullable restore +#nullable disable + /// + public System.TypeCode GetTypeCode() + { + return IsInitialized() ? Value.GetTypeCode() : default; + } + + /// + bool System.IConvertible.ToBoolean(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToBoolean(provider) : default; + } + + /// + char System.IConvertible.ToChar(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToChar(provider) : default; + } + + /// + sbyte System.IConvertible.ToSByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSByte(provider) : default; + } + + /// + byte System.IConvertible.ToByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToByte(provider) : default; + } + + /// + short System.IConvertible.ToInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt16(provider) : default; + } + + /// + ushort System.IConvertible.ToUInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt16(provider) : default; + } + + /// + int System.IConvertible.ToInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt32(provider) : default; + } + + /// + uint System.IConvertible.ToUInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt32(provider) : default; + } + + /// + long System.IConvertible.ToInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt64(provider) : default; + } + + /// + ulong System.IConvertible.ToUInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt64(provider) : default; + } + + /// + float System.IConvertible.ToSingle(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSingle(provider) : default; + } + + /// + double System.IConvertible.ToDouble(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDouble(provider) : default; + } + + /// + decimal System.IConvertible.ToDecimal(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDecimal(provider) : default; + } + + /// + System.DateTime System.IConvertible.ToDateTime(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDateTime(provider) : default; + } + + /// + object System.IConvertible.ToType(global::System.Type @type, global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToType(@type, provider) : default; + } + +#nullable restore + public override global::System.Int32 GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + global::System.Int32 hash = (global::System.Int32)2166136261; + hash = (hash * 16777619) ^ GetType().GetHashCode(); + hash = (hash * 16777619) ^ global::System.StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + return hash; + } + } + + /// + public override global::System.String ToString() => IsInitialized() ? Value.ToString() ?? "" : "[UNINITIALIZED]"; + /// + public global::System.String ToString(global::System.IFormatProvider provider) => IsInitialized() ? Value.ToString(provider) ?? "" : "[UNINITIALIZED]"; + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private void EnsureInitialized() + { + if (!IsInitialized()) + { +#if DEBUG + ThrowHelper.ThrowWhenNotInitialized(_stackTrace); +#else + ThrowHelper.ThrowWhenNotInitialized(); +#endif + } + } + +#nullable disable + /// + /// Converts a StringThing to or from JSON. + /// + public partial class StringThingSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override StringThing Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + +#if NET6_0_OR_GREATER + public override StringThing ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WritePropertyName(value.Value); + } +#endif +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + private static void ThrowJsonExceptionWhenValidationFails(global::Vogen.Validation validation) + { + var e = ThrowHelper.CreateValidationException(validation); + throw new global::System.Text.Json.JsonException(null, e); + } + + private static void ThrowJsonExceptionWhenNull(global::System.String value) + { + if (value == null) + { + var e = ThrowHelper.CreateCannotBeNullException(); + throw new global::System.Text.Json.JsonException(null, e); + } + } + + private static StringThing DeserializeJson(global::System.String value) + { + ThrowJsonExceptionWhenNull(value); + return new StringThing(value); + } + } + +#nullable restore +#nullable disable + class StringThingTypeConverter : global::System.ComponentModel.TypeConverter + { + public override global::System.Boolean CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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) + { + var stringValue = value as global::System.String; + if (stringValue != null) + { + return StringThing.__Deserialize(stringValue); + } + + return base.ConvertFrom(context, culture, value); + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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 StringThing idValue) + { + if (destinationType == typeof(global::System.String)) + { + return idValue.Value; + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + +#nullable restore + internal sealed class StringThingDebugView + { + private readonly StringThing _t; + StringThingDebugView(StringThing t) + { + _t = t; + } + + public global::System.String UnderlyingType => "global::System.String"; + public global::System.String Value => _t.Value; + public global::System.String Conversions => @"[global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] +[global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] +"; + } + + static class ThrowHelper + { +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowInvalidOperationException(string message) => throw new global::System.InvalidOperationException(message); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowArgumentException(string message, string arg) => throw new global::System.ArgumentException(message, arg); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenCreatedWithNull() => throw CreateCannotBeNullException(); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized() => throw CreateValidationException("Use of uninitialized Value Object."); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized(global::System.Diagnostics.StackTrace stackTrace) => throw CreateValidationException("Use of uninitialized Value Object at: " + stackTrace ?? ""); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenValidationFails(global::Vogen.Validation validation) + { + throw CreateValidationException(validation); + } + + internal static global::System.Exception CreateValidationException(string message) => new global::Vogen.ValueObjectValidationException(message); + internal static global::System.Exception CreateCannotBeNullException() => new global::Vogen.ValueObjectValidationException("Cannot create a value object with null."); + internal static global::System.Exception CreateValidationException(global::Vogen.Validation validation) + { + var ex = CreateValidationException(validation.ErrorMessage); + if (validation.Data != null) + { + foreach (var kvp in validation.Data) + { + ex.Data[kvp.Key] = kvp.Value; + } + } + + return ex; + } + } + } +} +] \ No newline at end of file diff --git a/tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt b/tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt new file mode 100644 index 00000000000..62b4cd9b562 --- /dev/null +++ b/tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_comparers_class_with_default_comparison_when_both_are_specified.verified.txt @@ -0,0 +1,620 @@ +[ +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace generator +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + public class VogenTypesFactory : global::System.Text.Json.Serialization.JsonConverterFactory + { + public VogenTypesFactory() + { + } + + private static readonly global::System.Collections.Generic.Dictionary> _lookup = new global::System.Collections.Generic.Dictionary> + { + { + typeof(global::Whatever.StringThing), + new global::System.Lazy(() => new global::Whatever.StringThing.StringThingSystemTextJsonConverter()) + } + }; + public override bool CanConvert(global::System.Type typeToConvert) => _lookup.ContainsKey(typeToConvert); + public override global::System.Text.Json.Serialization.JsonConverter CreateConverter(global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) => _lookup[typeToConvert].Value; + } +} + +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace Whatever +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + [global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] + [global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] + [global::System.Diagnostics.DebuggerTypeProxyAttribute(typeof(StringThingDebugView))] + [global::System.Diagnostics.DebuggerDisplayAttribute("Underlying type: global::System.String, Value = { _value }")] + public partial class StringThing : global::System.IEquatable, global::System.IEquatable, global::System.IComparable, global::System.IComparable, global::System.IParsable, global::System.IConvertible + { +#if DEBUG +private readonly global::System.Diagnostics.StackTrace _stackTrace = null!; +#endif +#if !VOGEN_NO_VALIDATION + private readonly global::System.Boolean _isInitialized; +#endif + private readonly global::System.String _value; + /// + /// Gets the underlying value if set, otherwise a is thrown. + /// + public global::System.String Value + { + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get + { + EnsureInitialized(); + return _value; + } + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public StringThing() + { +#if DEBUG + _stackTrace = new global::System.Diagnostics.StackTrace(); +#endif +#if !VOGEN_NO_VALIDATION + _isInitialized = false; +#endif + _value = default; + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private StringThing(global::System.String value) + { + _value = value; +#if !VOGEN_NO_VALIDATION + _isInitialized = true; +#endif + } + + /// + /// Builds an instance from the provided underlying type. + /// + /// The underlying type. + /// An instance of this type. + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static StringThing From(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + /// + /// Tries to build an instance from the provided underlying type. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, false will be returned. + /// + /// The underlying type. + /// An instance of the value object. + /// True if the value object can be built, otherwise false. + public static bool TryFrom( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String value, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing vo) + { + if (value is null) + { + vo = default; + return false; + } + + vo = new StringThing(value); + return true; + } + + /// + /// Tries to build an instance from the provided underlying value. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, an error will be returned. + /// + /// The primitive value. + /// A containing either the value object, or an error. + public static global::Vogen.ValueObjectOrError TryFrom(global::System.String value) + { + if (value is null) + { + return new global::Vogen.ValueObjectOrError(global::Vogen.Validation.Invalid("The value provided was null")); + } + + return new global::Vogen.ValueObjectOrError(new StringThing(value)); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] +#if VOGEN_NO_VALIDATION +#pragma warning disable CS8775 + public bool IsInitialized() => true; +#pragma warning restore CS8775 +#else + public bool IsInitialized() => _isInitialized; +#endif +#nullable disable + public static class Comparers + { + private class __StringEqualityComparer : global::System.Collections.Generic.IEqualityComparer + { + readonly global::System.StringComparer _comparer; + public __StringEqualityComparer(global::System.StringComparer comparer) + { + _comparer = comparer; + } + + public bool Equals(StringThing x, StringThing y) + { + return _comparer.Equals(x._value, y._value); + } + + public int GetHashCode(StringThing obj) + { + return _comparer.GetHashCode(obj._value); + } + } + + public static global::System.Collections.Generic.IEqualityComparer Ordinal => new __StringEqualityComparer(global::System.StringComparer.Ordinal); + public static global::System.Collections.Generic.IEqualityComparer OrdinalIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.OrdinalIgnoreCase); + public static global::System.Collections.Generic.IEqualityComparer CurrentCulture => new __StringEqualityComparer(global::System.StringComparer.CurrentCulture); + public static global::System.Collections.Generic.IEqualityComparer CurrentCultureIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.CurrentCultureIgnoreCase); + public static global::System.Collections.Generic.IEqualityComparer InvariantCulture => new __StringEqualityComparer(global::System.StringComparer.InvariantCulture); + public static global::System.Collections.Generic.IEqualityComparer InvariantCultureIgnoreCase => new __StringEqualityComparer(global::System.StringComparer.InvariantCultureIgnoreCase); +#nullable restore + } + + // only called internally when something has been deserialized into + // its primitive type. + private static StringThing __Deserialize(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + public global::System.Boolean Equals(StringThing other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + // It's possible to create uninitialized instances via converters such as EfCore (HasDefaultValue), which call Equals. + // We treat anything uninitialized as not equal to anything, even other uninitialized instances of this type. + if (!IsInitialized() || !other.IsInitialized()) + return false; + if (ReferenceEquals(this, other)) + { + return true; + } + + return GetType() == other.GetType() && global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, other.Value); + } + + public global::System.Boolean Equals(StringThing other, global::System.Collections.Generic.IEqualityComparer comparer) + { + return comparer.Equals(this, other); + } + + public global::System.Boolean Equals(global::System.String primitive) + { + return global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, primitive); + } + + public global::System.Boolean Equals(global::System.String primitive, global::System.StringComparer comparer) + { + return comparer.Equals(Value, primitive); + } + + public override global::System.Boolean Equals(global::System.Object obj) + { + return Equals(obj as StringThing); + } + + public static global::System.Boolean operator ==(StringThing left, StringThing right) => Equals(left, right); + public static global::System.Boolean operator !=(StringThing left, StringThing right) => !Equals(left, right); + public static global::System.Boolean operator ==(StringThing left, global::System.String right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left.Value, right); + public static global::System.Boolean operator ==(global::System.String left, StringThing right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left, right.Value); + public static global::System.Boolean operator !=(global::System.String left, StringThing right) => !(left == right); + public static global::System.Boolean operator !=(StringThing left, global::System.String right) => !(left == right); + public static explicit operator StringThing(global::System.String value) => From(value); + public static explicit operator global::System.String(StringThing value) => value.Value; + public int CompareTo(StringThing other) + { + if (other is null) + return 1; + return Value.CompareTo(other.Value); + } + + public int CompareTo(object other) + { + if (other is null) + return 1; + if (other is StringThing x) + return CompareTo(x); + ThrowHelper.ThrowArgumentException("Cannot compare to object as it is not of type StringThing", nameof(other)); + return 0; + } + + /// + /// + /// + /// True if the value passes any validation (after running any optional normalization). + /// + public static global::System.Boolean TryParse( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String s, global::System.IFormatProvider provider, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing result) + { + if (s is null) + { + result = default; + return false; + } + + result = new StringThing(s); + return true; + } + + /// + /// + /// + /// The value created via the method. + /// + /// Thrown when the value can be parsed, but is not valid. + public static StringThing Parse(global::System.String s, global::System.IFormatProvider provider) + { + return From(s); + } + +#nullable disable +#nullable restore +#nullable disable + /// + public System.TypeCode GetTypeCode() + { + return IsInitialized() ? Value.GetTypeCode() : default; + } + + /// + bool System.IConvertible.ToBoolean(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToBoolean(provider) : default; + } + + /// + byte System.IConvertible.ToByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToByte(provider) : default; + } + + /// + char System.IConvertible.ToChar(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToChar(provider) : default; + } + + /// + System.DateTime System.IConvertible.ToDateTime(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDateTime(provider) : default; + } + + /// + decimal System.IConvertible.ToDecimal(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDecimal(provider) : default; + } + + /// + double System.IConvertible.ToDouble(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDouble(provider) : default; + } + + /// + short System.IConvertible.ToInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt16(provider) : default; + } + + /// + int System.IConvertible.ToInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt32(provider) : default; + } + + /// + long System.IConvertible.ToInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt64(provider) : default; + } + + /// + sbyte System.IConvertible.ToSByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSByte(provider) : default; + } + + /// + float System.IConvertible.ToSingle(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSingle(provider) : default; + } + + /// + object System.IConvertible.ToType(global::System.Type @type, global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToType(@type, provider) : default; + } + + /// + ushort System.IConvertible.ToUInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt16(provider) : default; + } + + /// + uint System.IConvertible.ToUInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt32(provider) : default; + } + + /// + ulong System.IConvertible.ToUInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt64(provider) : default; + } + +#nullable restore + public override global::System.Int32 GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + global::System.Int32 hash = (global::System.Int32)2166136261; + hash = (hash * 16777619) ^ GetType().GetHashCode(); + hash = (hash * 16777619) ^ global::System.StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + return hash; + } + } + + /// + public override global::System.String ToString() => IsInitialized() ? Value.ToString() ?? "" : "[UNINITIALIZED]"; + /// + public global::System.String ToString(global::System.IFormatProvider provider) => IsInitialized() ? Value.ToString(provider) ?? "" : "[UNINITIALIZED]"; + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private void EnsureInitialized() + { + if (!IsInitialized()) + { +#if DEBUG + ThrowHelper.ThrowWhenNotInitialized(_stackTrace); +#else + ThrowHelper.ThrowWhenNotInitialized(); +#endif + } + } + +#nullable disable + /// + /// Converts a StringThing to or from JSON. + /// + public partial class StringThingSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override StringThing Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + +#if NET6_0_OR_GREATER + public override StringThing ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WritePropertyName(value.Value); + } +#endif +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + private static void ThrowJsonExceptionWhenValidationFails(global::Vogen.Validation validation) + { + var e = ThrowHelper.CreateValidationException(validation); + throw new global::System.Text.Json.JsonException(null, e); + } + + private static void ThrowJsonExceptionWhenNull(global::System.String value) + { + if (value == null) + { + var e = ThrowHelper.CreateCannotBeNullException(); + throw new global::System.Text.Json.JsonException(null, e); + } + } + + private static StringThing DeserializeJson(global::System.String value) + { + ThrowJsonExceptionWhenNull(value); + return new StringThing(value); + } + } + +#nullable restore +#nullable disable + class StringThingTypeConverter : global::System.ComponentModel.TypeConverter + { + public override global::System.Boolean CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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) + { + var stringValue = value as global::System.String; + if (stringValue != null) + { + return StringThing.__Deserialize(stringValue); + } + + return base.ConvertFrom(context, culture, value); + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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 StringThing idValue) + { + if (destinationType == typeof(global::System.String)) + { + return idValue.Value; + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + +#nullable restore + internal sealed class StringThingDebugView + { + private readonly StringThing _t; + StringThingDebugView(StringThing t) + { + _t = t; + } + + public global::System.String UnderlyingType => "global::System.String"; + public global::System.String Value => _t.Value; + public global::System.String Conversions => @"[global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] +[global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] +"; + } + + static class ThrowHelper + { +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowInvalidOperationException(string message) => throw new global::System.InvalidOperationException(message); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowArgumentException(string message, string arg) => throw new global::System.ArgumentException(message, arg); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenCreatedWithNull() => throw CreateCannotBeNullException(); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized() => throw CreateValidationException("Use of uninitialized Value Object."); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized(global::System.Diagnostics.StackTrace stackTrace) => throw CreateValidationException("Use of uninitialized Value Object at: " + stackTrace ?? ""); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenValidationFails(global::Vogen.Validation validation) + { + throw CreateValidationException(validation); + } + + internal static global::System.Exception CreateValidationException(string message) => new global::Vogen.ValueObjectValidationException(message); + internal static global::System.Exception CreateCannotBeNullException() => new global::Vogen.ValueObjectValidationException("Cannot create a value object with null."); + internal static global::System.Exception CreateValidationException(global::Vogen.Validation validation) + { + var ex = CreateValidationException(validation.ErrorMessage); + if (validation.Data != null) + { + foreach (var kvp in validation.Data) + { + ex.Data[kvp.Key] = kvp.Value; + } + } + + return ex; + } + } + } +} +] \ No newline at end of file diff --git a/tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_when_specified.verified.txt b/tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_when_specified.verified.txt index 351990388f5..26f2975c54d 100644 --- a/tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_when_specified.verified.txt +++ b/tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_when_specified.verified.txt @@ -208,7 +208,7 @@ private readonly global::System.Diagnostics.StackTrace _stackTrace = null!; public int GetHashCode(StringThing obj) { - return _comparer.GetHashCode(); + return _comparer.GetHashCode(obj._value); } } diff --git a/tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt b/tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt new file mode 100644 index 00000000000..461a0c4ec01 --- /dev/null +++ b/tests/SnapshotTests/StringComparison/snapshots/snap-v8.0/StringComparisonGenerationTests.Generates_with_default_comparison_OrdinalIgnoreCase.verified.txt @@ -0,0 +1,589 @@ +[ +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace generator +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + public class VogenTypesFactory : global::System.Text.Json.Serialization.JsonConverterFactory + { + public VogenTypesFactory() + { + } + + private static readonly global::System.Collections.Generic.Dictionary> _lookup = new global::System.Collections.Generic.Dictionary> + { + { + typeof(global::Whatever.StringThing), + new global::System.Lazy(() => new global::Whatever.StringThing.StringThingSystemTextJsonConverter()) + } + }; + public override bool CanConvert(global::System.Type typeToConvert) => _lookup.ContainsKey(typeToConvert); + public override global::System.Text.Json.Serialization.JsonConverter CreateConverter(global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) => _lookup[typeToConvert].Value; + } +} + +// ------------------------------------------------------------------------------ +// +// This code was generated by a source generator named Vogen (https://github.com/SteveDunn/Vogen) +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 +// Suppress warnings for 'Override methods on comparable types'. +#pragma warning disable CA1036 +// Suppress Error MA0097 : A class that implements IComparable or IComparable should override comparison operators +#pragma warning disable MA0097 +// Suppress warning for 'The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.' +// The generator copies signatures from the BCL, e.g. for `TryParse`, and some of those have nullable annotations. +#pragma warning disable CS8669, CS8632 +#pragma warning disable CS8604 // Possible null reference argument. + +// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' +#pragma warning disable CS1591 +// Suppress warnings about CS8767 : Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). +#pragma warning disable CS8767 +namespace Whatever +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Vogen", "1.0.0.0")] + [global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] + [global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] + [global::System.Diagnostics.DebuggerTypeProxyAttribute(typeof(StringThingDebugView))] + [global::System.Diagnostics.DebuggerDisplayAttribute("Underlying type: global::System.String, Value = { _value }")] + public partial class StringThing : global::System.IEquatable, global::System.IEquatable, global::System.IComparable, global::System.IComparable, global::System.IParsable, global::System.IConvertible + { +#if DEBUG +private readonly global::System.Diagnostics.StackTrace _stackTrace = null!; +#endif +#if !VOGEN_NO_VALIDATION + private readonly global::System.Boolean _isInitialized; +#endif + private readonly global::System.String _value; + /// + /// Gets the underlying value if set, otherwise a is thrown. + /// + public global::System.String Value + { + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get + { + EnsureInitialized(); + return _value; + } + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public StringThing() + { +#if DEBUG + _stackTrace = new global::System.Diagnostics.StackTrace(); +#endif +#if !VOGEN_NO_VALIDATION + _isInitialized = false; +#endif + _value = default; + } + + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private StringThing(global::System.String value) + { + _value = value; +#if !VOGEN_NO_VALIDATION + _isInitialized = true; +#endif + } + + /// + /// Builds an instance from the provided underlying type. + /// + /// The underlying type. + /// An instance of this type. + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static StringThing From(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + /// + /// Tries to build an instance from the provided underlying type. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, false will be returned. + /// + /// The underlying type. + /// An instance of the value object. + /// True if the value object can be built, otherwise false. + public static bool TryFrom( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String value, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing vo) + { + if (value is null) + { + vo = default; + return false; + } + + vo = new StringThing(value); + return true; + } + + /// + /// Tries to build an instance from the provided underlying value. + /// If a normalization method is provided, it will be called. + /// If validation is provided, and it fails, an error will be returned. + /// + /// The primitive value. + /// A containing either the value object, or an error. + public static global::Vogen.ValueObjectOrError TryFrom(global::System.String value) + { + if (value is null) + { + return new global::Vogen.ValueObjectOrError(global::Vogen.Validation.Invalid("The value provided was null")); + } + + return new global::Vogen.ValueObjectOrError(new StringThing(value)); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] +#if VOGEN_NO_VALIDATION +#pragma warning disable CS8775 + public bool IsInitialized() => true; +#pragma warning restore CS8775 +#else + public bool IsInitialized() => _isInitialized; +#endif + // only called internally when something has been deserialized into + // its primitive type. + private static StringThing __Deserialize(global::System.String value) + { + if (value is null) + { + ThrowHelper.ThrowWhenCreatedWithNull(); + return default !; + } + + return new StringThing(value); + } + + public global::System.Boolean Equals(StringThing other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + // It's possible to create uninitialized instances via converters such as EfCore (HasDefaultValue), which call Equals. + // We treat anything uninitialized as not equal to anything, even other uninitialized instances of this type. + if (!IsInitialized() || !other.IsInitialized()) + return false; + if (ReferenceEquals(this, other)) + { + return true; + } + + return GetType() == other.GetType() && global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, other.Value); + } + + public global::System.Boolean Equals(StringThing other, global::System.Collections.Generic.IEqualityComparer comparer) + { + return comparer.Equals(this, other); + } + + public global::System.Boolean Equals(global::System.String primitive) + { + return global::System.StringComparer.OrdinalIgnoreCase.Equals(Value, primitive); + } + + public global::System.Boolean Equals(global::System.String primitive, global::System.StringComparer comparer) + { + return comparer.Equals(Value, primitive); + } + + public override global::System.Boolean Equals(global::System.Object obj) + { + return Equals(obj as StringThing); + } + + public static global::System.Boolean operator ==(StringThing left, StringThing right) => Equals(left, right); + public static global::System.Boolean operator !=(StringThing left, StringThing right) => !Equals(left, right); + public static global::System.Boolean operator ==(StringThing left, global::System.String right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left.Value, right); + public static global::System.Boolean operator ==(global::System.String left, StringThing right) => global::System.StringComparer.OrdinalIgnoreCase.Equals(left, right.Value); + public static global::System.Boolean operator !=(global::System.String left, StringThing right) => !(left == right); + public static global::System.Boolean operator !=(StringThing left, global::System.String right) => !(left == right); + public static explicit operator StringThing(global::System.String value) => From(value); + public static explicit operator global::System.String(StringThing value) => value.Value; + public int CompareTo(StringThing other) + { + if (other is null) + return 1; + return Value.CompareTo(other.Value); + } + + public int CompareTo(object other) + { + if (other is null) + return 1; + if (other is StringThing x) + return CompareTo(x); + ThrowHelper.ThrowArgumentException("Cannot compare to object as it is not of type StringThing", nameof(other)); + return 0; + } + + /// + /// + /// + /// True if the value passes any validation (after running any optional normalization). + /// + public static global::System.Boolean TryParse( +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + global::System.String s, global::System.IFormatProvider provider, +#if NETCOREAPP3_0_OR_GREATER +[global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] +#endif + out StringThing result) + { + if (s is null) + { + result = default; + return false; + } + + result = new StringThing(s); + return true; + } + + /// + /// + /// + /// The value created via the method. + /// + /// Thrown when the value can be parsed, but is not valid. + public static StringThing Parse(global::System.String s, global::System.IFormatProvider provider) + { + return From(s); + } + +#nullable disable +#nullable restore +#nullable disable + /// + public System.TypeCode GetTypeCode() + { + return IsInitialized() ? Value.GetTypeCode() : default; + } + + /// + bool System.IConvertible.ToBoolean(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToBoolean(provider) : default; + } + + /// + byte System.IConvertible.ToByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToByte(provider) : default; + } + + /// + char System.IConvertible.ToChar(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToChar(provider) : default; + } + + /// + System.DateTime System.IConvertible.ToDateTime(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDateTime(provider) : default; + } + + /// + decimal System.IConvertible.ToDecimal(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDecimal(provider) : default; + } + + /// + double System.IConvertible.ToDouble(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToDouble(provider) : default; + } + + /// + short System.IConvertible.ToInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt16(provider) : default; + } + + /// + int System.IConvertible.ToInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt32(provider) : default; + } + + /// + long System.IConvertible.ToInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToInt64(provider) : default; + } + + /// + sbyte System.IConvertible.ToSByte(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSByte(provider) : default; + } + + /// + float System.IConvertible.ToSingle(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToSingle(provider) : default; + } + + /// + object System.IConvertible.ToType(global::System.Type @type, global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToType(@type, provider) : default; + } + + /// + ushort System.IConvertible.ToUInt16(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt16(provider) : default; + } + + /// + uint System.IConvertible.ToUInt32(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt32(provider) : default; + } + + /// + ulong System.IConvertible.ToUInt64(global::System.IFormatProvider provider) + { + return IsInitialized() ? (Value as System.IConvertible).ToUInt64(provider) : default; + } + +#nullable restore + public override global::System.Int32 GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + global::System.Int32 hash = (global::System.Int32)2166136261; + hash = (hash * 16777619) ^ GetType().GetHashCode(); + hash = (hash * 16777619) ^ global::System.StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + return hash; + } + } + + /// + public override global::System.String ToString() => IsInitialized() ? Value.ToString() ?? "" : "[UNINITIALIZED]"; + /// + public global::System.String ToString(global::System.IFormatProvider provider) => IsInitialized() ? Value.ToString(provider) ?? "" : "[UNINITIALIZED]"; + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private void EnsureInitialized() + { + if (!IsInitialized()) + { +#if DEBUG + ThrowHelper.ThrowWhenNotInitialized(_stackTrace); +#else + ThrowHelper.ThrowWhenNotInitialized(); +#endif + } + } + +#nullable disable + /// + /// Converts a StringThing to or from JSON. + /// + public partial class StringThingSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override StringThing Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + +#if NET6_0_OR_GREATER + public override StringThing ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return DeserializeJson(reader.GetString()); + } + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, StringThing value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WritePropertyName(value.Value); + } +#endif +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + private static void ThrowJsonExceptionWhenValidationFails(global::Vogen.Validation validation) + { + var e = ThrowHelper.CreateValidationException(validation); + throw new global::System.Text.Json.JsonException(null, e); + } + + private static void ThrowJsonExceptionWhenNull(global::System.String value) + { + if (value == null) + { + var e = ThrowHelper.CreateCannotBeNullException(); + throw new global::System.Text.Json.JsonException(null, e); + } + } + + private static StringThing DeserializeJson(global::System.String value) + { + ThrowJsonExceptionWhenNull(value); + return new StringThing(value); + } + } + +#nullable restore +#nullable disable + class StringThingTypeConverter : global::System.ComponentModel.TypeConverter + { + public override global::System.Boolean CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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) + { + var stringValue = value as global::System.String; + if (stringValue != null) + { + return StringThing.__Deserialize(stringValue); + } + + return base.ConvertFrom(context, culture, value); + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext context, global::System.Type sourceType) + { + return 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 StringThing idValue) + { + if (destinationType == typeof(global::System.String)) + { + return idValue.Value; + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + +#nullable restore + internal sealed class StringThingDebugView + { + private readonly StringThing _t; + StringThingDebugView(StringThing t) + { + _t = t; + } + + public global::System.String UnderlyingType => "global::System.String"; + public global::System.String Value => _t.Value; + public global::System.String Conversions => @"[global::System.Text.Json.Serialization.JsonConverter(typeof(StringThingSystemTextJsonConverter))] +[global::System.ComponentModel.TypeConverter(typeof(StringThingTypeConverter))] +"; + } + + static class ThrowHelper + { +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowInvalidOperationException(string message) => throw new global::System.InvalidOperationException(message); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowArgumentException(string message, string arg) => throw new global::System.ArgumentException(message, arg); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenCreatedWithNull() => throw CreateCannotBeNullException(); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized() => throw CreateValidationException("Use of uninitialized Value Object."); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenNotInitialized(global::System.Diagnostics.StackTrace stackTrace) => throw CreateValidationException("Use of uninitialized Value Object at: " + stackTrace ?? ""); +#if NETCOREAPP3_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute] +#endif + internal static void ThrowWhenValidationFails(global::Vogen.Validation validation) + { + throw CreateValidationException(validation); + } + + internal static global::System.Exception CreateValidationException(string message) => new global::Vogen.ValueObjectValidationException(message); + internal static global::System.Exception CreateCannotBeNullException() => new global::Vogen.ValueObjectValidationException("Cannot create a value object with null."); + internal static global::System.Exception CreateValidationException(global::Vogen.Validation validation) + { + var ex = CreateValidationException(validation.ErrorMessage); + if (validation.Data != null) + { + foreach (var kvp in validation.Data) + { + ex.Data[kvp.Key] = kvp.Value; + } + } + + return ex; + } + } + } +} +] \ No newline at end of file