From 6b8ae0d7a46d0c5dba84432780604fa3b2ee392b Mon Sep 17 00:00:00 2001
From: Timo van Zijll Langhout <655426+Timovzl@users.noreply.github.com>
Date: Sun, 27 Jul 2025 15:09:15 +0200
Subject: [PATCH 01/23] .NET 8+ only, IIdentity implements IWrapperValueObject,
bug fixes, trimming, warnings.
---
.../DomainModeling.Example.csproj | 2 +-
.../DummyBuilderGenerator.cs | 26 +++++++++----------
DomainModeling.Generator/IdentityGenerator.cs | 12 ++++-----
.../TypeSymbolExtensions.cs | 15 +++++++++++
.../ValueObjectGenerator.cs | 16 +++++++-----
.../WrapperValueObjectGenerator.cs | 12 ++++-----
.../DomainModeling.Tests.csproj | 2 +-
.../Configuration/IDomainEventConfigurator.cs | 2 ++
.../Configuration/IEntityConfigurator.cs | 2 ++
.../Configuration/IIdentityConfigurator.cs | 2 ++
.../IWrapperValueObjectConfigurator.cs | 2 ++
.../Conversions/DomainObjectSerializer.cs | 26 ++++++++++++++-----
DomainModeling/DomainModeling.csproj | 15 ++++++++---
DomainModeling/IIdentity.cs | 14 +++++++++-
DomainModeling/IValueObject.cs | 5 +---
DomainModeling/IWrapperValueObject.cs | 15 ++++++++---
16 files changed, 117 insertions(+), 51 deletions(-)
diff --git a/DomainModeling.Example/DomainModeling.Example.csproj b/DomainModeling.Example/DomainModeling.Example.csproj
index 3b6d12e..b8f0085 100644
--- a/DomainModeling.Example/DomainModeling.Example.csproj
+++ b/DomainModeling.Example/DomainModeling.Example.csproj
@@ -2,7 +2,7 @@
Exe
- net6.0
+ net8.0
Architect.DomainModeling.Example
Architect.DomainModeling.Example
Enable
diff --git a/DomainModeling.Generator/DummyBuilderGenerator.cs b/DomainModeling.Generator/DummyBuilderGenerator.cs
index 55768a0..6e5b2e7 100644
--- a/DomainModeling.Generator/DummyBuilderGenerator.cs
+++ b/DomainModeling.Generator/DummyBuilderGenerator.cs
@@ -49,8 +49,8 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
var result = new Builder()
{
- TypeFullyQualifiedName = type.ToString(),
- ModelTypeFullyQualifiedName = modelType.ToString(),
+ TypeFullMetadataName = type.GetFullMetadataName(),
+ ModelTypeFullMetadataName = modelType is INamedTypeSymbol namedModelType ? namedModelType.GetFullMetadataName() : modelType.ToString(),
IsPartial = tds.Modifiers.Any(SyntaxKind.PartialKeyword),
IsRecord = type.IsRecord,
IsClass = type.TypeKind == TypeKind.Class,
@@ -91,15 +91,15 @@ private static void GenerateSource(SourceProductionContext context, (ImmutableAr
var concreteBuilderTypesByModel = builders
.Where(builder => !builder.IsAbstract && !builder.IsGeneric) // Concrete only
- .GroupBy(builder => builder.ModelTypeFullyQualifiedName) // Deduplicate
- .Select(group => new KeyValuePair(compilation.GetTypeByMetadataName(group.Key), group.First().TypeFullyQualifiedName))
- .Where(pair => pair.Key is not null)
+ .GroupBy(builder => builder.ModelTypeFullMetadataName) // Deduplicate
+ .Select(group => new KeyValuePair(compilation.GetTypeByMetadataName(group.Key), compilation.GetTypeByMetadataName(group.First().TypeFullMetadataName)?.ToString()!))
+ .Where(pair => pair.Key is not null && pair.Value is not null)
.ToDictionary, ITypeSymbol, string>(pair => pair.Key!, pair => pair.Value, SymbolEqualityComparer.Default);
// Remove models for which multiple builders exist
{
var buildersWithDuplicateModel = builders
- .GroupBy(builder => builder.ModelTypeFullyQualifiedName)
+ .GroupBy(builder => builder.ModelTypeFullMetadataName)
.Where(group => group.Count() > 1)
.ToList();
@@ -110,7 +110,7 @@ private static void GenerateSource(SourceProductionContext context, (ImmutableAr
builders.Remove(type);
context.ReportDiagnostic("DummyBuilderGeneratorDuplicateBuilders", "Duplicate builders",
- $"Multiple dummy builders exist for {group.Key}. Source generation for these builders was skipped.", DiagnosticSeverity.Warning, compilation.GetTypeByMetadataName(group.Last().TypeFullyQualifiedName));
+ $"Multiple dummy builders exist for {group.Key}. Source generation for these builders was skipped.", DiagnosticSeverity.Warning, compilation.GetTypeByMetadataName(group.Last().TypeFullMetadataName));
}
}
@@ -118,7 +118,7 @@ private static void GenerateSource(SourceProductionContext context, (ImmutableAr
{
context.CancellationToken.ThrowIfCancellationRequested();
- var type = compilation.GetTypeByMetadataName(builder.TypeFullyQualifiedName);
+ var type = compilation.GetTypeByMetadataName(builder.TypeFullMetadataName);
var modelType = type?.GetAttribute("DummyBuilderAttribute", Constants.DomainModelingNamespace, arity: 1) is AttributeData { AttributeClass: not null } attribute
? attribute.AttributeClass.TypeArguments[0]
: null;
@@ -135,7 +135,7 @@ private static void GenerateSource(SourceProductionContext context, (ImmutableAr
if (type is null)
{
context.ReportDiagnostic("DummyBuilderGeneratorUnexpectedType", "Unexpected type",
- $"Type marked as dummy builder has unexpected type '{builder.TypeFullyQualifiedName}'.", DiagnosticSeverity.Warning, type);
+ $"Type marked as dummy builder has unexpected type '{builder.TypeFullMetadataName}'.", DiagnosticSeverity.Warning, type);
continue;
}
@@ -143,7 +143,7 @@ private static void GenerateSource(SourceProductionContext context, (ImmutableAr
if (modelType is null)
{
context.ReportDiagnostic("DummyBuilderGeneratorUnexpectedModelType", "Unexpected model type",
- $"Type marked as dummy builder has unexpected model type '{builder.ModelTypeFullyQualifiedName}'.", DiagnosticSeverity.Warning, type);
+ $"Type marked as dummy builder has unexpected model type '{builder.ModelTypeFullMetadataName}'.", DiagnosticSeverity.Warning, type);
continue;
}
@@ -218,7 +218,7 @@ private static void GenerateSource(SourceProductionContext context, (ImmutableAr
componentBuilder.Append("// ");
componentBuilder.AppendLine($" private {param.Type.WithNullableAnnotation(NullableAnnotation.None)} {memberName} {{ get; set; }} = {param.Type.CreateDummyInstantiationExpression(param.Name == "value" ? param.ContainingType.Name : param.Name, concreteBuilderTypesByModel.Keys, type => $"new {concreteBuilderTypesByModel[type]}().Build()")};");
- concreteBuilderTypesByModel.Add(modelType, builder.TypeFullyQualifiedName);
+ concreteBuilderTypesByModel.Add(modelType, builder.TypeFullMetadataName);
}
if (membersByName[$"With{memberName}"].Any(member => member is IMethodSymbol method && method.Parameters.Length == 1 && method.Parameters[0].Type.Equals(param.Type, SymbolEqualityComparer.Default)))
@@ -336,8 +336,8 @@ namespace {containingNamespace}
private sealed record Builder : IGeneratable
{
- public string TypeFullyQualifiedName { get; set; } = null!;
- public string ModelTypeFullyQualifiedName { get; set; } = null!;
+ public string TypeFullMetadataName { get; set; } = null!;
+ public string ModelTypeFullMetadataName { get; set; } = null!;
public bool IsPartial { get; set; }
public bool IsRecord { get; set; }
public bool IsClass { get; set; }
diff --git a/DomainModeling.Generator/IdentityGenerator.cs b/DomainModeling.Generator/IdentityGenerator.cs
index 5992e1e..fb1f883 100644
--- a/DomainModeling.Generator/IdentityGenerator.cs
+++ b/DomainModeling.Generator/IdentityGenerator.cs
@@ -124,12 +124,12 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
!ctor.IsStatic && ctor.Parameters.Length == 1 && ctor.Parameters[0].Type.Equals(underlyingType, SymbolEqualityComparer.Default)));
// Records override this, but our implementation is superior
- existingComponents |= IdTypeComponents.ToStringOverride.If(!result.IsRecord && members.Any(member =>
- member.Name == nameof(ToString) && member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 0));
+ existingComponents |= IdTypeComponents.ToStringOverride.If(members.Any(member =>
+ member.Name == nameof(ToString) && member is IMethodSymbol { IsImplicitlyDeclared: false } method && method.Arity == 0 && method.Parameters.Length == 0));
// Records override this, but our implementation is superior
- existingComponents |= IdTypeComponents.GetHashCodeOverride.If(!result.IsRecord && members.Any(member =>
- member.Name == nameof(GetHashCode) && member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 0));
+ existingComponents |= IdTypeComponents.GetHashCodeOverride.If(members.Any(member =>
+ member.Name == nameof(GetHashCode) && member is IMethodSymbol { IsImplicitlyDeclared: false } method && method.Arity == 0 && method.Parameters.Length == 0));
// Records irrevocably and correctly override this, checking the type and delegating to IEquatable.Equals(T)
existingComponents |= IdTypeComponents.EqualsOverride.If(members.Any(member =>
@@ -137,8 +137,8 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
method.Parameters[0].Type.IsType
@@ -97,10 +98,8 @@ Misc improvements
-
- True
-
-
+
+
@@ -108,17 +107,10 @@ Misc improvements
-
- false
- Content
- PreserveNewest
-
-
-
-
-
-
-
+
+
+
+
From 6e09698dbb32632e1fe92ca84d7ab9d7c5b64a0f Mon Sep 17 00:00:00 2001
From: Timo van Zijll Langhout <655426+Timovzl@users.noreply.github.com>
Date: Wed, 3 Sep 2025 23:21:59 +0200
Subject: [PATCH 09/23] Added EnumerableComparer overloads that avoid boxing
for ImmutableAray. (ArraySegment, the other common collection struct, has
implicit conversions from arrays, which mess with nullability constraints.)
---
.../Comparisons/EnumerableComparer.cs | 112 ++++++++++++------
1 file changed, 76 insertions(+), 36 deletions(-)
diff --git a/DomainModeling/Comparisons/EnumerableComparer.cs b/DomainModeling/Comparisons/EnumerableComparer.cs
index d874c24..19d0c16 100644
--- a/DomainModeling/Comparisons/EnumerableComparer.cs
+++ b/DomainModeling/Comparisons/EnumerableComparer.cs
@@ -1,5 +1,7 @@
using System.Collections;
+using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
namespace Architect.DomainModeling.Comparisons;
@@ -25,6 +27,20 @@ public static int GetEnumerableHashCode([AllowNull] IEnumerable enumerable)
return -1;
}
+ ///
+ ///
+ /// Returns a hash code over some of the content of the given .
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetEnumerableHashCode([AllowNull] ImmutableArray? enumerable)
+ {
+ if (enumerable is not {} value) return 0;
+ var span = value.AsSpan();
+ if (span.Length == 0) return 1;
+ return HashCode.Combine(span.Length, span[0], span[^1]);
+ }
+
///
///
/// Returns a hash code over some of the content of the given .
@@ -66,6 +82,59 @@ public static int GetEnumerableHashCode([AllowNull] IEnumerable
+ ///
+ /// Compares the given objects for equality by comparing their elements.
+ ///
+ ///
+ /// This method performs equality checks on the 's elements.
+ /// It is not recursive. To support nested collections, use custom collections that override their equality checks accordingly.
+ ///
+ ///
+ /// This non-generic overload should be avoided if possible.
+ /// It lacks the ability to special-case generic types, which may lead to unexpected results.
+ /// For example, two instances with an ignore-case comparer may consider each other equal despite having different-cased contents.
+ /// However, the current method has no knowledge of their comparers or their order-agnosticism, and may return a different result.
+ ///
+ ///
+ /// Unlike , this method may cause boxing of elements that are of a value type.
+ ///
+ ///
+ public static bool EnumerableEquals([AllowNull] IEnumerable left, [AllowNull] IEnumerable right)
+ {
+ if (ReferenceEquals(left, right)) return true;
+ if (left is null || right is null) return false; // Double nulls are already handled above
+
+ var rightEnumerator = right.GetEnumerator();
+ using (rightEnumerator as IDisposable)
+ {
+ foreach (var leftElement in left)
+ if (!rightEnumerator.MoveNext() || !Equals(leftElement, rightEnumerator.Current))
+ return false;
+ if (rightEnumerator.MoveNext()) return false;
+ }
+
+ return true;
+ }
+
+ ///
+ ///
+ /// Compares the given objects for equality by comparing their elements.
+ ///
+ ///
+ /// This method performs equality checks on the 's elements.
+ /// It is not recursive. To support nested collections, use custom collections that override their equality checks accordingly.
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool EnumerableEquals([AllowNull] ImmutableArray? left, [AllowNull] ImmutableArray? right)
+ {
+ if (left is not ImmutableArray leftValue || right is not ImmutableArray rightValue)
+ return left is null & right is null;
+
+ return MemoryExtensions.SequenceEqual(leftValue.AsSpan(), rightValue.AsSpan());
+ }
+
///
///
/// Compares the given objects for equality by comparing their elements.
@@ -89,7 +158,7 @@ public static bool EnumerableEquals([AllowNull] IEnumerable
return MemoryExtensions.SequenceEqual(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(leftList), System.Runtime.InteropServices.CollectionsMarshal.AsSpan(rightList));
if (left is TElement[] leftArray && right is TElement[] rightArray)
return MemoryExtensions.SequenceEqual(leftArray.AsSpan(), rightArray.AsSpan());
- if (left is System.Collections.Immutable.ImmutableArray leftImmutableArray && right is System.Collections.Immutable.ImmutableArray rightImmutableArray)
+ if (left is ImmutableArray leftImmutableArray && right is ImmutableArray rightImmutableArray)
return MemoryExtensions.SequenceEqual(leftImmutableArray.AsSpan(), rightImmutableArray.AsSpan());
// Prefer to index directly, to avoid allocation of an enumerator
@@ -149,41 +218,6 @@ static bool GenericEnumerableEquals(IEnumerable leftEnumerable, IEnume
}
}
- ///
- ///
- /// Compares the given objects for equality by comparing their elements.
- ///
- ///
- /// This method performs equality checks on the 's elements.
- /// It is not recursive. To support nested collections, use custom collections that override their equality checks accordingly.
- ///
- ///
- /// This non-generic overload should be avoided if possible.
- /// It lacks the ability to special-case generic types, which may lead to unexpected results.
- /// For example, two instances with an ignore-case comparer may consider each other equal despite having different-cased contents.
- /// However, the current method has no knowledge of their comparers or their order-agnosticism, and may return a different result.
- ///
- ///
- /// Unlike , this method may cause boxing of elements that are of a value type.
- ///
- ///
- public static bool EnumerableEquals([AllowNull] IEnumerable left, [AllowNull] IEnumerable right)
- {
- if (ReferenceEquals(left, right)) return true;
- if (left is null || right is null) return false; // Double nulls are already handled above
-
- var rightEnumerator = right.GetEnumerator();
- using (rightEnumerator as IDisposable)
- {
- foreach (var leftElement in left)
- if (!rightEnumerator.MoveNext() || !Equals(leftElement, rightEnumerator.Current))
- return false;
- if (rightEnumerator.MoveNext()) return false;
- }
-
- return true;
- }
-
///
///
/// Returns a hash code over some of the content of the given wrapped in a .
@@ -192,6 +226,7 @@ public static bool EnumerableEquals([AllowNull] IEnumerable left, [AllowNull] IE
/// For a corresponding equality check, use .
///
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetMemoryHashCode(Memory? memory)
{
return GetMemoryHashCode((ReadOnlyMemory?)memory);
@@ -205,6 +240,7 @@ public static int GetMemoryHashCode(Memory? memory)
/// For a corresponding equality check, use .
///
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetMemoryHashCode(ReadOnlyMemory? memory)
{
if (memory is null) return 0;
@@ -219,6 +255,7 @@ public static int GetMemoryHashCode(ReadOnlyMemory? memory)
/// For a corresponding equality check, use .
///
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetMemoryHashCode(Memory memory)
{
return GetMemoryHashCode((ReadOnlyMemory)memory);
@@ -232,6 +269,7 @@ public static int GetMemoryHashCode(Memory memory)
/// For a corresponding equality check, use .
///
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetMemoryHashCode(ReadOnlyMemory memory)
{
return GetSpanHashCode(memory.Span);
@@ -245,6 +283,7 @@ public static int GetMemoryHashCode(ReadOnlyMemory memory)
/// For a corresponding equality check, use .
///
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetSpanHashCode(Span span)
{
return GetSpanHashCode((ReadOnlySpan)span);
@@ -258,6 +297,7 @@ public static int GetSpanHashCode(Span span)
/// For a corresponding equality check, use .
///
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetSpanHashCode(ReadOnlySpan span)
{
// Note that we do not distinguish between a default span and a regular empty span
From 6bfe159947ff5b9910298c01f9a5daee8cf02268 Mon Sep 17 00:00:00 2001
From: Timo van Zijll Langhout <655426+Timovzl@users.noreply.github.com>
Date: Sat, 6 Sep 2025 01:23:15 +0200
Subject: [PATCH 10/23] Added serialization to/from deepest underlying type
(recursive).
Also cleaned up the way in which lines not being source generated are outcommented.
Also improved source generator performance.
Also prevented transient issues when reporting warnings on wrapper types.
---
...inModelConfiguratorGenerator.Identities.cs | 15 +-
...nfiguratorGenerator.WrapperValueObjects.cs | 15 +-
.../EntityFrameworkConfigurationGenerator.cs | 60 ++-
...ns.cs => DiagnosticReportingExtensions.cs} | 2 +-
DomainModeling.Generator/EnumExtensions.cs | 24 +-
DomainModeling.Generator/IdentityGenerator.cs | 382 +++++++-------
DomainModeling.Generator/SymbolExtensions.cs | 15 +-
.../TypeSymbolExtensions.cs | 8 +-
.../ValueObjectGenerator.cs | 50 +-
.../ValueWrapperGenerator.cs | 79 ++-
.../WrapperValueObjectGenerator.cs | 489 +++++++++---------
...ityFrameworkConfigurationGeneratorTests.cs | 78 ++-
DomainModeling.Tests/IdentityTests.cs | 138 ++++-
.../WrapperValueObjectTests.cs | 146 +++++-
.../Configuration/IDomainEventConfigurator.cs | 3 +-
.../Configuration/IEntityConfigurator.cs | 3 +-
.../Configuration/IIdentityConfigurator.cs | 9 +-
.../IWrapperValueObjectConfigurator.cs | 9 +-
.../Conversions/DomainObjectSerializer.cs | 20 +-
DomainModeling/Conversions/IValueWrapper.cs | 38 --
.../Conversions/ObjectInstantiator.cs | 4 +
.../ValueWrapperFormattingExtensions.cs | 1 +
.../Conversions/ValueWrapperJsonConverter.cs | 4 +-
.../ValueWrapperNewtonsoftJsonConverter.cs | 4 +-
.../ValueWrapperParsingExtensions.cs | 1 +
.../Conversions/ValueWrapperUnwrapper.cs | 49 ++
DomainModeling/DomainModeling.csproj | 19 +-
DomainModeling/ISerializableDomainObject.cs | 1 +
DomainModeling/IValueWrapper.cs | 109 ++++
29 files changed, 1173 insertions(+), 602 deletions(-)
rename DomainModeling.Generator/{SourceProductionContextExtensions.cs => DiagnosticReportingExtensions.cs} (96%)
delete mode 100644 DomainModeling/Conversions/IValueWrapper.cs
create mode 100644 DomainModeling/Conversions/ValueWrapperUnwrapper.cs
create mode 100644 DomainModeling/IValueWrapper.cs
diff --git a/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Identities.cs b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Identities.cs
index 8c1bc27..a8ce5c0 100644
--- a/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Identities.cs
+++ b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Identities.cs
@@ -7,19 +7,22 @@ public partial class DomainModelConfiguratorGenerator
{
internal static void GenerateSourceForIdentities(
SourceProductionContext context,
- (ImmutableArray Generatables, (bool HasConfigureConventions, string AssemblyName) Metadata) input)
+ (ImmutableArray ValueWrappers, (bool HasConfigureConventions, string AssemblyName) Metadata) input)
{
context.CancellationToken.ThrowIfCancellationRequested();
- // Generate the method only if we have any generatables, or if we are an assembly in which ConfigureConventions() is called
- if (!input.Generatables.Any() && !input.Metadata.HasConfigureConventions)
+ // Generate the method only if we have any value wrappers, or if we are an assembly in which ConfigureConventions() is called
+ if (!input.ValueWrappers.Any() && !input.Metadata.HasConfigureConventions)
return;
var targetNamespace = input.Metadata.AssemblyName;
- var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.Generatables.Select(generatable => $"""
- configurator.ConfigureIdentity<{generatable.ContainingNamespace}.{generatable.TypeName}, {generatable.UnderlyingTypeFullyQualifiedName}>({Environment.NewLine} new Architect.DomainModeling.Configuration.IIdentityConfigurator.Args());
- """));
+ var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.ValueWrappers
+ .Where(generatable => generatable.IsIdentity)
+ .Select(generatable => (Generatable: generatable, CoreTypeName: ValueWrapperGenerator.GetCoreTypeFullyQualifiedName(input.ValueWrappers, generatable.TypeName, generatable.ContainingNamespace)))
+ .Select(tuple => $$"""
+ configurator.ConfigureIdentity<{{tuple.Generatable.ContainingNamespace}}.{{tuple.Generatable.TypeName}}, {{tuple.Generatable.UnderlyingTypeFullyQualifiedName}}, {{tuple.CoreTypeName}}>({{Environment.NewLine}} new Architect.DomainModeling.Configuration.IIdentityConfigurator.Args());
+ """));
var source = $@"
using Architect.DomainModeling;
diff --git a/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.WrapperValueObjects.cs b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.WrapperValueObjects.cs
index 32cd6a9..98d56bd 100644
--- a/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.WrapperValueObjects.cs
+++ b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.WrapperValueObjects.cs
@@ -7,19 +7,22 @@ public partial class DomainModelConfiguratorGenerator
{
internal static void GenerateSourceForWrapperValueObjects(
SourceProductionContext context,
- (ImmutableArray Generatables, (bool HasConfigureConventions, string AssemblyName) Metadata) input)
+ (ImmutableArray ValueWrappers, (bool HasConfigureConventions, string AssemblyName) Metadata) input)
{
context.CancellationToken.ThrowIfCancellationRequested();
- // Generate the method only if we have any generatables, or if we are an assembly in which ConfigureConventions() is called
- if (!input.Generatables.Any() && !input.Metadata.HasConfigureConventions)
+ // Generate the method only if we have any value wrappers, or if we are an assembly in which ConfigureConventions() is called
+ if (!input.ValueWrappers.Any() && !input.Metadata.HasConfigureConventions)
return;
var targetNamespace = input.Metadata.AssemblyName;
- var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.Generatables.Select(generatable => $"""
- configurator.ConfigureWrapperValueObject<{generatable.ContainingNamespace}.{generatable.TypeName}, {generatable.UnderlyingTypeFullyQualifiedName}>({Environment.NewLine} new Architect.DomainModeling.Configuration.IWrapperValueObjectConfigurator.Args());
- """));
+ var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.ValueWrappers
+ .Where(generatable => !generatable.IsIdentity)
+ .Select(generatable => (Generatable: generatable, CoreTypeName: ValueWrapperGenerator.GetCoreTypeFullyQualifiedName(input.ValueWrappers, generatable.TypeName, generatable.ContainingNamespace)))
+ .Select(tuple => $$"""
+ configurator.ConfigureWrapperValueObject<{{tuple.Generatable.ContainingNamespace}}.{{tuple.Generatable.TypeName}}, {{tuple.Generatable.UnderlyingTypeFullyQualifiedName}}, {{tuple.CoreTypeName}}>({{Environment.NewLine}} new Architect.DomainModeling.Configuration.IWrapperValueObjectConfigurator.Args());
+ """));
var source = $@"
using Architect.DomainModeling;
diff --git a/DomainModeling.Generator/Configurators/EntityFrameworkConfigurationGenerator.cs b/DomainModeling.Generator/Configurators/EntityFrameworkConfigurationGenerator.cs
index e4b592d..5987b80 100644
--- a/DomainModeling.Generator/Configurators/EntityFrameworkConfigurationGenerator.cs
+++ b/DomainModeling.Generator/Configurators/EntityFrameworkConfigurationGenerator.cs
@@ -259,34 +259,36 @@ file sealed record class DomainModelConfigurator(
file sealed record class EntityFrameworkIdentityConfigurator(ModelConfigurationBuilder ConfigurationBuilder)
: Architect.DomainModeling.Configuration.IIdentityConfigurator
{{
- public void ConfigureIdentity<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TIdentity, TUnderlying>(
- in Architect.DomainModeling.Configuration.IIdentityConfigurator.Args _)
- where TIdentity : IIdentity, ISerializableDomainObject
+ private static readonly ConverterMappingHints DecimalIdConverterMappingHints = new ConverterMappingHints(precision: 28, scale: 0); // For decimal IDs
+
+ public void ConfigureIdentity<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TIdentity, TUnderlying, TCore>(
+ in Architect.DomainModeling.Configuration.IIdentityConfigurator.Args args)
+ where TIdentity : IIdentity, IDirectValueWrapper, ICoreValueWrapper
where TUnderlying : notnull, IEquatable, IComparable
{{
// Configure properties of the type
this.ConfigurationBuilder.Properties()
- .HaveConversion>();
+ .HaveConversion>();
// Configure non-property occurrences of the type, such as in CAST(), SUM(), AVG(), etc.
this.ConfigurationBuilder.DefaultTypeMapping()
- .HasConversion>();
+ .HasConversion>();
// The converter's mapping hints are currently ignored by DefaultTypeMapping, which is probably a bug: https://github.com/dotnet/efcore/issues/32533
- if (typeof(TUnderlying) == typeof(decimal))
+ if (typeof(TCore) == typeof(decimal))
this.ConfigurationBuilder.DefaultTypeMapping()
.HasPrecision(28, 0);
}}
private sealed class IdentityValueObjectConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TModel, TProvider>
: ValueConverter
- where TModel : ISerializableDomainObject
+ where TModel : IValueWrapper
{{
public IdentityValueObjectConverter()
: base(
- DomainObjectSerializer.CreateSerializeExpression(),
- DomainObjectSerializer.CreateDeserializeExpression(),
- new ConverterMappingHints(precision: 28, scale: 0)) // For decimal IDs
+ model => DomainObjectSerializer.Serialize(model)!,
+ provider => DomainObjectSerializer.Deserialize(provider)!,
+ typeof(TProvider) == typeof(decimal) ? DecimalIdConverterMappingHints : null)
{{
}}
}}
@@ -296,28 +298,28 @@ file sealed record class EntityFrameworkWrapperValueObjectConfigurator(
ModelConfigurationBuilder ConfigurationBuilder)
: Architect.DomainModeling.Configuration.IWrapperValueObjectConfigurator
{{
- public void ConfigureWrapperValueObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TWrapper, TValue>(
- in Architect.DomainModeling.Configuration.IWrapperValueObjectConfigurator.Args _)
- where TWrapper : IWrapperValueObject, ISerializableDomainObject
+ public void ConfigureWrapperValueObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TWrapper, TValue, TCore>(
+ in Architect.DomainModeling.Configuration.IWrapperValueObjectConfigurator.Args args)
+ where TWrapper : IWrapperValueObject, IDirectValueWrapper, ICoreValueWrapper
where TValue : notnull
{{
// Configure properties of the type
this.ConfigurationBuilder.Properties()
- .HaveConversion>();
+ .HaveConversion>();
// Configure non-property occurrences of the type, such as in CAST(), SUM(), AVG(), etc.
this.ConfigurationBuilder.DefaultTypeMapping()
- .HasConversion>();
+ .HasConversion>();
}}
private sealed class WrapperValueObjectConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TModel, TProvider>
: ValueConverter
- where TModel : ISerializableDomainObject
+ where TModel : IValueWrapper
{{
public WrapperValueObjectConverter()
: base(
- DomainObjectSerializer.CreateSerializeExpression(),
- DomainObjectSerializer.CreateDeserializeExpression())
+ model => DomainObjectSerializer.Serialize(model)!,
+ provider => DomainObjectSerializer.Deserialize(provider)!)
{{
}}
}}
@@ -350,7 +352,7 @@ public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConven
#pragma warning disable EF1001 // Internal EF Core API usage -- No public APIs are available for this yet, and interceptors do not work because EF demands a usable ctor even the interceptor would prevent ctor usage
var entityType = entityTypeConvention as EntityType ?? throw new NotImplementedException($""{{entityTypeConvention.GetType().Name}} was received when {{nameof(EntityType)}} was expected. Either a non-entity was passed or internal changes to Entity Framework have broken this code."");
- entityType.ConstructorBinding = new UninitializedInstantiationBinding(typeof(TEntity), DomainObjectSerializer.CreateDeserializeExpression(typeof(TEntity)));
+ entityType.ConstructorBinding = UninitializedInstantiationBinding.Create(() => DomainObjectSerializer.Deserialize());
#pragma warning restore EF1001 // Internal EF Core API usage
}}
@@ -363,7 +365,7 @@ public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConven
#pragma warning disable EF1001 // Internal EF Core API usage -- No public APIs are available for this yet, and interceptors do not work because EF demands a usable ctor even the interceptor would prevent ctor usage
var entityType = entityTypeConvention as EntityType ?? throw new NotImplementedException($""{{entityTypeConvention.GetType().Name}} was received when {{nameof(EntityType)}} was expected. Either a non-entity was passed or internal changes to Entity Framework have broken this code."");
- entityType.ConstructorBinding = new UninitializedInstantiationBinding(typeof(TDomainEvent), DomainObjectSerializer.CreateDeserializeExpression(typeof(TDomainEvent)));
+ entityType.ConstructorBinding = UninitializedInstantiationBinding.Create(() => DomainObjectSerializer.Deserialize());
#pragma warning restore EF1001 // Internal EF Core API usage
}}
@@ -373,7 +375,13 @@ private sealed class UninitializedInstantiationBinding
private static readonly MethodInfo GetUninitializedObjectMethod = typeof(RuntimeHelpers).GetMethod(nameof(RuntimeHelpers.GetUninitializedObject))!;
public override Type RuntimeType {{ get; }}
- private Expression? Expression {{ get; }}
+ private Expression Expression {{ get; }}
+
+ public static UninitializedInstantiationBinding Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(
+ Expression> expression)
+ {{
+ return new UninitializedInstantiationBinding(typeof(T), expression.Body);
+ }}
public UninitializedInstantiationBinding(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type runtimeType,
@@ -381,15 +389,15 @@ public UninitializedInstantiationBinding(
: base(Array.Empty())
{{
this.RuntimeType = runtimeType;
- this.Expression = expression;
+ this.Expression = expression ??
+ Expression.Convert(
+ Expression.Call(method: GetUninitializedObjectMethod, arguments: Expression.Constant(this.RuntimeType)),
+ this.RuntimeType);
}}
public override Expression CreateConstructorExpression(ParameterBindingInfo bindingInfo)
{{
- return this.Expression ??
- Expression.Convert(
- Expression.Call(method: GetUninitializedObjectMethod, arguments: Expression.Constant(this.RuntimeType)),
- this.RuntimeType);
+ return this.Expression;
}}
public override InstantiationBinding With(IReadOnlyList parameterBindings)
diff --git a/DomainModeling.Generator/SourceProductionContextExtensions.cs b/DomainModeling.Generator/DiagnosticReportingExtensions.cs
similarity index 96%
rename from DomainModeling.Generator/SourceProductionContextExtensions.cs
rename to DomainModeling.Generator/DiagnosticReportingExtensions.cs
index 58748b7..08dc26d 100644
--- a/DomainModeling.Generator/SourceProductionContextExtensions.cs
+++ b/DomainModeling.Generator/DiagnosticReportingExtensions.cs
@@ -6,7 +6,7 @@ namespace Architect.DomainModeling.Generator;
///
/// Defines extension methods on .
///
-internal static class SourceProductionContextExtensions
+internal static class DiagnosticReportingExtensions
{
///
/// Shorthand extension method to report a diagnostic, with less boilerplate code.
diff --git a/DomainModeling.Generator/EnumExtensions.cs b/DomainModeling.Generator/EnumExtensions.cs
index 8084ff4..6234962 100644
--- a/DomainModeling.Generator/EnumExtensions.cs
+++ b/DomainModeling.Generator/EnumExtensions.cs
@@ -59,10 +59,14 @@ public static string ToCodeString(this Accessibility accessibility, string unspe
/// myEnum |= MyEnum.SomeFlag.If(1 == 2);
///
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T If(this T enumValue, bool condition)
where T : unmanaged, Enum
{
- return condition ? enumValue : default;
+ // Branch-free implementation
+ ReadOnlySpan values = stackalloc T[] { default, enumValue, };
+ var index = Unsafe.As(ref condition);
+ return values[index];
}
///
@@ -77,15 +81,20 @@ public static T If(this T enumValue, bool condition)
/// myEnum |= MyEnum.SomeFlag.Unless(1 == 2);
///
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Unless(this T enumValue, bool condition)
where T : unmanaged, Enum
{
- return condition ? default : enumValue;
+ // Branch-free implementation
+ ReadOnlySpan values = stackalloc T[] { enumValue, default, };
+ var index = Unsafe.As(ref condition);
+ return values[index];
}
///
- /// Efficiently returns whether the has the given set.
+ /// Efficiently returns whether the has the given (s) set.
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool HasFlags(this T subject, T flag)
where T : unmanaged, Enum
{
@@ -107,11 +116,8 @@ public static bool HasFlags(this T subject, T flag)
private static ulong GetNumericValue(T enumValue)
where T : unmanaged, Enum
{
- Span ulongSpan = stackalloc ulong[] { 0UL };
- var span = MemoryMarshal.Cast(ulongSpan);
-
- span[0] = enumValue;
-
- return ulongSpan[0];
+ var result = 0UL;
+ Unsafe.WriteUnaligned(ref Unsafe.As(ref result), enumValue);
+ return result;
}
}
diff --git a/DomainModeling.Generator/IdentityGenerator.cs b/DomainModeling.Generator/IdentityGenerator.cs
index ec72779..eaf1ac0 100644
--- a/DomainModeling.Generator/IdentityGenerator.cs
+++ b/DomainModeling.Generator/IdentityGenerator.cs
@@ -1,5 +1,4 @@
using System.Collections.Immutable;
-using Architect.DomainModeling.Generator.Common;
using Architect.DomainModeling.Generator.Configurators;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -29,9 +28,11 @@ internal void InitializeBasicProvider(IncrementalGeneratorInitializationContext
INamedTypeSymbol type when LooksLikeEntity(type) && IsEntity(type, out var entityInterface) && entityInterface.TypeArguments[0].TypeKind == TypeKind.Error &&
entityInterface.TypeArguments[1] is ITypeSymbol underlyingType =>
new ValueWrapperGenerator.BasicGeneratable(
+ isIdentity: true,
typeName: entityInterface.TypeArguments[0].Name,
containingNamespace: type.ContainingNamespace.ToString(),
underlyingTypeFullyQualifiedName: underlyingType.ToString(),
+ customCoreTypeFullyQualifiedName: type.AllInterfaces.FirstOrDefault(interf => interf.IsType("ICoreValueWrapper", "Architect", "DomainModeling", arity: 2))?.TypeArguments[1].ToString(),
isSpanFormattable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
interf is { Name: "ISpanFormattable", ContainingNamespace.Name: "System", Arity: 0, }),
isSpanParsable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
@@ -41,18 +42,22 @@ INamedTypeSymbol type when LooksLikeEntity(type) && IsEntity(type, out var entit
isUtf8SpanParsable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
interf is { Name: "IUtf8SpanParsable", ContainingNamespace.Name: "System", Arity: 1, })),
INamedTypeSymbol type when HasRequiredAttribute(type, out var attribute) && attribute.AttributeClass!.TypeArguments[0] is ITypeSymbol underlyingType =>
- new ValueWrapperGenerator.BasicGeneratable(
- typeName: type.Name,
- containingNamespace: type.ContainingNamespace.ToString(),
- underlyingTypeFullyQualifiedName: underlyingType.ToString(),
- isSpanFormattable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
- interf is { Name: "ISpanFormattable", ContainingNamespace.Name: "System", Arity: 0, }),
- isSpanParsable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
- interf is { Name: "ISpanParsable", ContainingNamespace.Name: "System", Arity: 1, }),
- isUtf8SpanFormattable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
- interf is { Name: "IUtf8SpanFormattable", ContainingNamespace.Name: "System", Arity: 0, }),
- isUtf8SpanParsable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
- interf is { Name: "IUtf8SpanParsable", ContainingNamespace.Name: "System", Arity: 1, })),
+ GetFirstProblem((TypeDeclarationSyntax)context.Node, type, underlyingType) is { }
+ ? default
+ : new ValueWrapperGenerator.BasicGeneratable(
+ isIdentity: true,
+ typeName: type.Name,
+ containingNamespace: type.ContainingNamespace.ToString(),
+ underlyingTypeFullyQualifiedName: underlyingType.ToString(),
+ customCoreTypeFullyQualifiedName: type.AllInterfaces.FirstOrDefault(interf => interf.IsType("ICoreValueWrapper", "Architect", "DomainModeling", arity: 2))?.TypeArguments[1].ToString(),
+ isSpanFormattable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
+ interf is { Name: "ISpanFormattable", ContainingNamespace.Name: "System", Arity: 0, }),
+ isSpanParsable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
+ interf is { Name: "ISpanParsable", ContainingNamespace.Name: "System", Arity: 1, }),
+ isUtf8SpanFormattable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
+ interf is { Name: "IUtf8SpanFormattable", ContainingNamespace.Name: "System", Arity: 0, }),
+ isUtf8SpanParsable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
+ interf is { Name: "IUtf8SpanParsable", ContainingNamespace.Name: "System", Arity: 1, })),
_ => default,
})
.Where(generatable => generatable != default)
@@ -64,8 +69,8 @@ INamedTypeSymbol type when HasRequiredAttribute(type, out var attribute) && attr
/// Additionally gathers detailed info per individual identity.
/// Generates source based on all of the above.
///
- internal void Generate(IncrementalGeneratorInitializationContext context,
- IncrementalValueProvider> identities,
+ internal void Generate(
+ IncrementalGeneratorInitializationContext context,
IncrementalValueProvider> valueWrappers)
{
var provider = context.SyntaxProvider.CreateSyntaxProvider(FilterSyntaxNode, TransformSyntaxNode)
@@ -74,7 +79,7 @@ internal void Generate(IncrementalGeneratorInitializationContext context,
context.RegisterSourceOutput(provider.Combine(valueWrappers), GenerateSource!);
- var aggregatedProvider = identities.Combine(EntityFrameworkConfigurationGenerator.CreateMetadataProvider(context));
+ var aggregatedProvider = valueWrappers.Combine(EntityFrameworkConfigurationGenerator.CreateMetadataProvider(context));
context.RegisterSourceOutput(aggregatedProvider, DomainModelConfiguratorGenerator.GenerateSourceForIdentities);
}
@@ -99,6 +104,66 @@ private static bool HasRequiredAttribute(INamedTypeSymbol type, out AttributeDat
return attribute != null;
}
+ private static Diagnostic? GetFirstProblem(TypeDeclarationSyntax tds, INamedTypeSymbol type, ITypeSymbol underlyingType)
+ {
+ var isPartial = tds.Modifiers.Any(SyntaxKind.PartialKeyword);
+
+ // Require the expected inheritance
+ if (!isPartial && !type.IsOrImplementsInterface(interf => interf.IsType("IIdentity", "Architect", "DomainModeling", arity: 1), out _))
+ return CreateDiagnostic("IdentityGeneratorUnexpectedInheritance", "Unexpected interface",
+ "Type marked as identity value object lacks IIdentity interface. Did you forget the 'partial' keyword and elude source generation?", DiagnosticSeverity.Warning);
+
+ // Require IDirectValueWrapper
+ var hasDirectValueWrapperInterface = type.AllInterfaces.Any(interf =>
+ interf.IsType("IDirectValueWrapper", "Architect", "DomainModeling", arity: 2) && !interf.IsImplicitlyDeclared &&
+ interf.TypeArguments[0].Equals(type, SymbolEqualityComparer.Default) && interf.TypeArguments[1].Equals(underlyingType, SymbolEqualityComparer.Default));
+ if (!isPartial && !hasDirectValueWrapperInterface)
+ return CreateDiagnostic("IdentityGeneratorMissingDirectValueWrapper", "Missing interface",
+ $"Type marked as identity value object lacks IDirectValueWrapper<{type.Name}, {underlyingType.Name}> interface.", DiagnosticSeverity.Warning);
+
+ // Require ICoreValueWrapper
+ var hasCoreValueWrapperInterface = type.AllInterfaces.Any(interf =>
+ interf.IsType("ICoreValueWrapper", "Architect", "DomainModeling", arity: 2) && !interf.IsImplicitlyDeclared &&
+ interf.TypeArguments[0].Equals(type, SymbolEqualityComparer.Default));
+ if (!isPartial && !hasCoreValueWrapperInterface)
+ return CreateDiagnostic("IdentityGeneratorMissingCoreValueWrapper", "Missing interface",
+ $"Type marked as identity value object lacks ICoreValueWrapper<{type.Name}, {underlyingType.Name}> interface.", DiagnosticSeverity.Warning);
+
+ // No source generation, only above analyzers
+ if (isPartial)
+ {
+ // Only if struct
+ if (type.TypeKind != TypeKind.Struct)
+ return CreateDiagnostic("IdentityGeneratorReferenceType", "Source-generated reference-typed identity",
+ "The type was not source-generated because it is a class, while a struct was expected. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning);
+
+ // Only if non-abstract
+ if (type.IsAbstract)
+ return CreateDiagnostic("IdentityGeneratorAbstractType", "Source-generated abstract type",
+ "The type was not source-generated because it is abstract. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning);
+
+ // Only if non-generic
+ if (type.IsGeneric())
+ return CreateDiagnostic("IdentityGeneratorGenericType", "Source-generated generic type",
+ "The type was not source-generated because it is generic. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning);
+
+ // Only if non-nested
+ if (type.IsNested())
+ return CreateDiagnostic("IdentityGeneratorNestedType", "Source-generated nested type",
+ "The type was not source-generated because it is a nested type. To get source generation, avoid nesting it inside another type. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning);
+ }
+
+ return null;
+
+ // Local shorthand to create a diagnostic
+ Diagnostic CreateDiagnostic(string id, string title, string description, DiagnosticSeverity severity)
+ {
+ return Diagnostic.Create(
+ new DiagnosticDescriptor(id, title, description, "Architect.DomainModeling", severity, isEnabledByDefault: true),
+ type.Locations.FirstOrDefault());
+ }
+ }
+
private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancellationToken = default)
{
// Struct or class or record
@@ -149,13 +214,18 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
var idType = entityInterface.TypeArguments[0];
underlyingType = entityInterface.TypeArguments[1];
result.EntityTypeName = type.Name;
- result.EntityTypeLocation = type.Locations.FirstOrDefault();
// The ID type exists if it is not of TypeKind.Error
result.IdTypeExists = idType.TypeKind != TypeKind.Error;
if (result.IdTypeExists)
+ {
+ // Entity was needlessly used, with a preexisting TId
+ result.Problem = Diagnostic.Create(new DiagnosticDescriptor("EntityIdentityTypeAlreadyExists", "Entity identity type already exists", "Architect.DomainModeling",
+ "Base class Entity is intended to generate source for TId, but TId refers to an existing type. To use an existing identity type, inherit from Entity instead.",
+ DiagnosticSeverity.Warning, isEnabledByDefault: true), type.Locations.FirstOrDefault());
return result;
+ }
result.IsStruct = true;
result.ContainingNamespace = type.ContainingNamespace.ToString();
@@ -175,9 +245,7 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
underlyingType = attribute.AttributeClass!.TypeArguments[0];
result.IdTypeExists = true;
- result.IdTypeLocation = type.Locations.FirstOrDefault();
result.IsIIdentity = type.IsOrImplementsInterface(interf => interf.IsType("IIdentity", "Architect", "DomainModeling", arity: 1), out _);
- result.IsSerializableDomainObject = type.IsOrImplementsInterface(type => type.IsType("ISerializableDomainObject", "Architect", "DomainModeling", arity: 2), out _);
result.IsPartial = tds.Modifiers.Any(SyntaxKind.PartialKeyword);
result.IsRecord = type.IsRecord;
result.IsStruct = type.TypeKind == TypeKind.Struct;
@@ -202,98 +270,90 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
// Records override this, but our implementation is superior
existingComponents |= IdTypeComponents.ToStringOverride.If(members.Any(member =>
- member.Name == nameof(ToString) && member is IMethodSymbol { IsImplicitlyDeclared: false } method && method.Arity == 0 && method.Parameters.Length == 0));
+ member is IMethodSymbol { Name: nameof(ToString), IsImplicitlyDeclared: false, IsOverride: true, Arity: 0, Parameters.Length: 0, }));
// Records override this, but our implementation is superior
existingComponents |= IdTypeComponents.GetHashCodeOverride.If(members.Any(member =>
- member.Name == nameof(GetHashCode) && member is IMethodSymbol { IsImplicitlyDeclared: false } method && method.Arity == 0 && method.Parameters.Length == 0));
+ member is IMethodSymbol { Name: nameof(GetHashCode), IsImplicitlyDeclared: false, IsOverride: true, Arity: 0, Parameters.Length: 0, }));
// Records irrevocably and correctly override this, checking the type and delegating to IEquatable.Equals(T)
existingComponents |= IdTypeComponents.EqualsOverride.If(members.Any(member =>
- member.Name == nameof(Equals) && member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 1 &&
+ member is IMethodSymbol { Name: nameof(Equals), IsOverride: true, Arity: 0, Parameters.Length: 1, } method &&
method.Parameters[0].Type.SpecialType == SpecialType.System_Object));
// Records override this, but our implementation is superior
existingComponents |= IdTypeComponents.EqualsMethod.If(members.Any(member =>
- member.Name == nameof(Equals) && member is IMethodSymbol { IsImplicitlyDeclared: false } method && method.Arity == 0 && method.Parameters.Length == 1 &&
+ member.HasNameOrExplicitInterfaceImplementationName(nameof(Equals)) && member is IMethodSymbol { IsImplicitlyDeclared: false, IsOverride: false, Arity: 0, Parameters.Length: 1, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= IdTypeComponents.CompareToMethod.If(members.Any(member =>
- member.Name == nameof(IComparable.CompareTo) && member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 1 &&
+ member.HasNameOrExplicitInterfaceImplementationName(nameof(IComparable.CompareTo)) && member is IMethodSymbol { Arity: 0, Parameters.Length: 1, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default)));
// Records irrevocably and correctly override this, delegating to IEquatable.Equals(T)
existingComponents |= IdTypeComponents.EqualsOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_Equality") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.EqualityOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
// Records irrevocably and correctly override this, delegating to IEquatable.Equals(T)
existingComponents |= IdTypeComponents.NotEqualsOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_Inequality") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.InequalityOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= IdTypeComponents.GreaterThanOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_GreaterThan") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.GreaterThanOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= IdTypeComponents.LessThanOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_LessThan") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.LessThanOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= IdTypeComponents.GreaterEqualsOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_GreaterThanOrEqual") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.GreaterThanOrEqualOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= IdTypeComponents.LessEqualsOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_LessThanOrEqual") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.LessThanOrEqualOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= IdTypeComponents.ConvertToOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 1 &&
- (member.HasNameOrExplicitInterfaceImplementationName("op_Implicit") || member.HasNameOrExplicitInterfaceImplementationName("op_Explicit")) &&
+ member is IMethodSymbol { MethodKind: MethodKind.Conversion, IsStatic: true, Parameters.Length: 1, } method &&
method.ReturnType.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[0].Type.Equals(underlyingType, SymbolEqualityComparer.Default)));
existingComponents |= IdTypeComponents.ConvertFromOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 1 &&
- (member.HasNameOrExplicitInterfaceImplementationName("op_Implicit") || member.HasNameOrExplicitInterfaceImplementationName("op_Explicit")) &&
+ member is IMethodSymbol { MethodKind: MethodKind.Conversion, IsStatic: true, Parameters.Length: 1, } method &&
method.ReturnType.Equals(underlyingType, SymbolEqualityComparer.Default) &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= IdTypeComponents.NullableConvertToOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 1 &&
- (member.HasNameOrExplicitInterfaceImplementationName("op_Implicit") || member.HasNameOrExplicitInterfaceImplementationName("op_Explicit")) &&
+ member is IMethodSymbol { MethodKind: MethodKind.Conversion, IsStatic: true, Parameters.Length: 1, } method &&
method.ReturnType.IsNullableOf(type) &&
(underlyingType.IsReferenceType
? method.Parameters[0].Type.Equals(underlyingType, SymbolEqualityComparer.Default)
: method.Parameters[0].Type.IsNullableOf(underlyingType))));
existingComponents |= IdTypeComponents.NullableConvertFromOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 1 &&
- (member.HasNameOrExplicitInterfaceImplementationName("op_Implicit") || member.HasNameOrExplicitInterfaceImplementationName("op_Explicit")) &&
+ member is IMethodSymbol { MethodKind: MethodKind.Conversion, IsStatic: true, Parameters.Length: 1, } method &&
(underlyingType.IsReferenceType
? method.ReturnType.Equals(underlyingType, SymbolEqualityComparer.Default)
: method.ReturnType.IsNullableOf(underlyingType) &&
method.Parameters[0].Type.IsNullableOf(type))));
existingComponents |= IdTypeComponents.SerializeToUnderlying.If(members.Any(member =>
- member.HasNameOrExplicitInterfaceImplementationName("Serialize") && member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 0));
+ member.HasNameOrExplicitInterfaceImplementationName("Serialize") && member is IMethodSymbol { Arity: 0, IsStatic: false, Parameters.Length: 0, } method &&
+ method.ReturnType.Equals(underlyingType, SymbolEqualityComparer.Default)));
existingComponents |= IdTypeComponents.DeserializeFromUnderlying.If(members.Any(member =>
- member.HasNameOrExplicitInterfaceImplementationName("Deserialize") && member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 1 &&
- method.Parameters[0].Type.Equals(underlyingType, SymbolEqualityComparer.Default)));
+ member.HasNameOrExplicitInterfaceImplementationName("Deserialize") && member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 1, } method &&
+ method.Parameters[0].Type.Equals(underlyingType, SymbolEqualityComparer.Default) &&
+ method.ReturnType.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= IdTypeComponents.SystemTextJsonConverter.If(type.GetAttributes().Any(attribute =>
attribute.AttributeClass?.IsTypeWithNamespace("JsonConverterAttribute", "System.Text.Json.Serialization") == true));
@@ -305,21 +365,21 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
member.Name == "StringComparison"));
existingComponents |= IdTypeComponents.FormattableToStringOverride.If(members.Any(member =>
- member.Name == nameof(IFormattable.ToString) && member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
+ member.HasNameOrExplicitInterfaceImplementationName("ToString") && member is IMethodSymbol { Arity: 0, IsStatic: false, Parameters.Length: 2, } method &&
method.Parameters[0].Type.SpecialType == SpecialType.System_String && method.Parameters[1].Type.IsSystemType("IFormatProvider")));
existingComponents |= IdTypeComponents.ParsableTryParseMethod.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 3 &&
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 3, } method &&
member.HasNameOrExplicitInterfaceImplementationName("TryParse") &&
method.Parameters[0].Type.SpecialType == SpecialType.System_String && method.Parameters[1].Type.IsSystemType("IFormatProvider") && method.Parameters[2].Type.Equals(type, SymbolEqualityComparer.Default) && method.Parameters[2].RefKind == RefKind.Out));
existingComponents |= IdTypeComponents.ParsableParseMethod.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 2, } method &&
member.HasNameOrExplicitInterfaceImplementationName("Parse") &&
method.Parameters[0].Type.SpecialType == SpecialType.System_String && method.Parameters[1].Type.IsSystemType("IFormatProvider")));
existingComponents |= IdTypeComponents.SpanFormattableTryFormatMethod.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 4 &&
+ member is IMethodSymbol { Arity: 0, IsStatic: false, Parameters.Length: 4, } method &&
member.HasNameOrExplicitInterfaceImplementationName("TryFormat") &&
method.Parameters[0].Type.IsSpanOfSpecialType(SpecialType.System_Char) &&
method.Parameters[1].Type.SpecialType == SpecialType.System_Int32 && method.Parameters[1].RefKind == RefKind.Out &&
@@ -327,20 +387,20 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
method.Parameters[3].Type.IsSystemType("IFormatProvider")));
existingComponents |= IdTypeComponents.SpanParsableTryParseMethod.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 3 &&
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 3, } method &&
member.HasNameOrExplicitInterfaceImplementationName("TryParse") &&
method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Char) &&
method.Parameters[1].Type.IsSystemType("IFormatProvider") &&
method.Parameters[2].Type.Equals(type, SymbolEqualityComparer.Default) && method.Parameters[2].RefKind == RefKind.Out));
existingComponents |= IdTypeComponents.SpanParsableParseMethod.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 2, } method &&
member.HasNameOrExplicitInterfaceImplementationName("Parse") &&
method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Char) &&
method.Parameters[1].Type.IsSystemType("IFormatProvider")));
existingComponents |= IdTypeComponents.Utf8SpanFormattableTryFormatMethod.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 4 &&
+ member is IMethodSymbol { Arity: 0, IsStatic: false, Parameters.Length: 4, } method &&
member.HasNameOrExplicitInterfaceImplementationName("TryFormat") &&
method.Parameters[0].Type.IsSpanOfSpecialType(SpecialType.System_Byte) &&
method.Parameters[1].Type.SpecialType == SpecialType.System_Int32 && method.Parameters[1].RefKind == RefKind.Out &&
@@ -348,24 +408,35 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
method.Parameters[3].Type.IsSystemType("IFormatProvider")));
existingComponents |= IdTypeComponents.Utf8SpanParsableTryParseMethod.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 3 &&
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 3, } method &&
member.HasNameOrExplicitInterfaceImplementationName("TryParse") &&
method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Byte) &&
method.Parameters[1].Type.IsSystemType("IFormatProvider") &&
method.Parameters[2].Type.Equals(type, SymbolEqualityComparer.Default) && method.Parameters[2].RefKind == RefKind.Out));
existingComponents |= IdTypeComponents.Utf8SpanParsableParseMethod.If(members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 2, } method &&
member.HasNameOrExplicitInterfaceImplementationName("Parse") &&
method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Byte) &&
method.Parameters[1].Type.IsSystemType("IFormatProvider")));
existingComponents |= IdTypeComponents.CreateMethod.If(members.Any(member =>
- member is IMethodSymbol method && method.IsStatic && method.Arity == 0 && method.Parameters.Length == 1 &&
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 1, } method &&
member.HasNameOrExplicitInterfaceImplementationName("Create") &&
- method.Parameters[0].Type.Equals(underlyingType, SymbolEqualityComparer.Default)));
+ method.Parameters[0].Type.Equals(underlyingType, SymbolEqualityComparer.Default) &&
+ method.ReturnType.Equals(type, SymbolEqualityComparer.Default)));
+
+ existingComponents |= IdTypeComponents.DirectValueWrapperInterface.If(type.AllInterfaces.Any(interf =>
+ interf.IsType("IDirectValueWrapper", "Architect", "DomainModeling", arity: 2) && !interf.IsImplicitlyDeclared &&
+ interf.TypeArguments[0].Equals(type, SymbolEqualityComparer.Default) && interf.TypeArguments[1].Equals(underlyingType, SymbolEqualityComparer.Default)));
+
+ existingComponents |= IdTypeComponents.CoreValueWrapperInterface.If(type.AllInterfaces.Any(interf =>
+ interf.IsType("ICoreValueWrapper", "Architect", "DomainModeling", arity: 2) && !interf.IsImplicitlyDeclared &&
+ interf.TypeArguments[0].Equals(type, SymbolEqualityComparer.Default)));
result.ExistingComponents = existingComponents;
+
+ result.Problem = GetFirstProblem(tds, type, underlyingType);
}
result.ToStringExpression = underlyingType.CreateValueToStringExpression();
@@ -383,7 +454,7 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
return result;
}
-
+
private static void GenerateSource(SourceProductionContext context, (Generatable Generatable, ImmutableArray ValueWrappers) input)
{
context.CancellationToken.ThrowIfCancellationRequested();
@@ -391,6 +462,12 @@ private static void GenerateSource(SourceProductionContext context, (Generatable
var generatable = input.Generatable;
var valueWrappers = input.ValueWrappers;
+ if (generatable.Problem is not null)
+ context.ReportDiagnostic(generatable.Problem);
+
+ if (generatable.Problem is not null || (!generatable.IsPartial && generatable.IdTypeExists))
+ return;
+
var containingNamespace = generatable.ContainingNamespace;
var idTypeName = generatable.IdTypeName;
var underlyingTypeFullyQualifiedName = generatable.UnderlyingTypeFullyQualifiedName;
@@ -409,72 +486,10 @@ private static void GenerateSource(SourceProductionContext context, (Generatable
var existingComponents = generatable.ExistingComponents;
var hasIdentityValueObjectAttribute = generatable.IdTypeExists;
- (var isSpanFormattable, var isSpanParsable, var isUtf8SpanFormattable, var isUtf8SpanParsable) = ValueWrapperGenerator.GetFormattabilityAndParsabilityRecursively(
- valueWrappers,
- typeName: idTypeName, containingNamespace: containingNamespace, underlyingTypeFullyQualifiedName: underlyingTypeFullyQualifiedName);
+ var coreTypeFullyQualifiedName = ValueWrapperGenerator.GetCoreTypeFullyQualifiedName(valueWrappers, idTypeName, containingNamespace);
- if (generatable.IdTypeExists)
- {
- // Entity was needlessly used, with a preexisting TId
- if (entityTypeName is not null)
- {
- context.ReportDiagnostic("EntityIdentityTypeAlreadyExists", "Entity identity type already exists",
- "Base class Entity is intended to generate source for TId, but TId refers to an existing type. To use an existing identity type, inherit from Entity instead.", DiagnosticSeverity.Warning, generatable.EntityTypeLocation);
- return;
- }
-
- // Require the expected inheritance
- if (!generatable.IsPartial && !generatable.IsIIdentity)
- {
- context.ReportDiagnostic("IdentityGeneratorUnexpectedInheritance", "Unexpected interface",
- "Type marked as identity value object lacks IIdentity interface. Did you forget the 'partial' keyword and elude source generation?", DiagnosticSeverity.Warning, generatable.IdTypeLocation);
- return;
- }
-
- // Require ISerializableDomainObject
- if (!generatable.IsPartial && !generatable.IsSerializableDomainObject)
- {
- context.ReportDiagnostic("IdentityGeneratorMissingSerializableDomainObject", "Missing interface",
- "Type marked as identity value object lacks ISerializableDomainObject interface.", DiagnosticSeverity.Warning, generatable.IdTypeLocation);
- return;
- }
-
- // No source generation, only above analyzers
- if (!generatable.IsPartial)
- return;
-
- // Only if struct
- if (!generatable.IsStruct)
- {
- context.ReportDiagnostic("IdentityGeneratorReferenceType", "Source-generated reference-typed identity",
- "The type was not source-generated because it is a class, while a struct was expected. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning, generatable.IdTypeLocation);
- return;
- }
-
- // Only if non-abstract
- if (generatable.IsAbstract)
- {
- context.ReportDiagnostic("IdentityGeneratorAbstractType", "Source-generated abstract type",
- "The type was not source-generated because it is abstract. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning, generatable.IdTypeLocation);
- return;
- }
-
- // Only if non-generic
- if (generatable.IsGeneric)
- {
- context.ReportDiagnostic("IdentityGeneratorGenericType", "Source-generated generic type",
- "The type was not source-generated because it is generic. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning, generatable.IdTypeLocation);
- return;
- }
-
- // Only if non-nested
- if (generatable.IsNested)
- {
- context.ReportDiagnostic("IdentityGeneratorNestedType", "Source-generated nested type",
- "The type was not source-generated because it is a nested type. To get source generation, avoid nesting it inside another type. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning, generatable.IdTypeLocation);
- return;
- }
- }
+ (var isSpanFormattable, var isSpanParsable, var isUtf8SpanFormattable, var isUtf8SpanParsable) = ValueWrapperGenerator.GetFormattabilityAndParsabilityRecursively(
+ valueWrappers, typeName: idTypeName, containingNamespace: containingNamespace);
var summary = entityTypeName is null ? null : $@"
///
@@ -502,6 +517,7 @@ private static void GenerateSource(SourceProductionContext context, (Generatable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
using Architect.DomainModeling;
using Architect.DomainModeling.Conversions;
@@ -511,25 +527,19 @@ namespace {containingNamespace}
{{
{summary}
- {(existingComponents.HasFlags(IdTypeComponents.SystemTextJsonConverter) ? "/*" : "")}
- {JsonSerializationGenerator.WriteJsonConverterAttribute(idTypeName, underlyingTypeFullyQualifiedName, numericAsString: underlyingTypeIsNumericUnsuitableForJson)}
- {(existingComponents.HasFlags(IdTypeComponents.SystemTextJsonConverter) ? "*/" : "")}
-
- {(existingComponents.HasFlags(IdTypeComponents.NewtonsoftJsonConverter) ? "/*" : "")}
- {JsonSerializationGenerator.WriteNewtonsoftJsonConverterAttribute(idTypeName, underlyingTypeFullyQualifiedName, numericAsString: underlyingTypeIsNumericUnsuitableForJson)}
- {(existingComponents.HasFlags(IdTypeComponents.NewtonsoftJsonConverter) ? "*/" : "")}
-
+ {(existingComponents.HasFlags(IdTypeComponents.SystemTextJsonConverter) ? "//" : "")}{JsonSerializationGenerator.WriteJsonConverterAttribute(idTypeName, underlyingTypeFullyQualifiedName, numericAsString: underlyingTypeIsNumericUnsuitableForJson)}
+ {(existingComponents.HasFlags(IdTypeComponents.NewtonsoftJsonConverter) ? "//" : "")}{JsonSerializationGenerator.WriteNewtonsoftJsonConverterAttribute(idTypeName, underlyingTypeFullyQualifiedName, numericAsString: underlyingTypeIsNumericUnsuitableForJson)}
{(hasIdentityValueObjectAttribute ? "" : $"[IdentityValueObject<{underlyingTypeFullyQualifiedName}>]")}
{(entityTypeName is null ? "/* Generated */ " : "")}{accessibility.ToCodeString()} readonly{(entityTypeName is null ? " partial" : "")}{(isRecord ? " record" : "")} struct {idTypeName} :
IIdentity<{underlyingTypeFullyQualifiedName}>,
- IValueWrapper<{idTypeName}, {underlyingTypeFullyQualifiedName}>,
IEquatable<{idTypeName}>,
IComparable<{idTypeName}>,
{(isSpanFormattable ? "" : "//")}ISpanFormattable, ISpanFormattable{formattableParsableWrapperSuffix},
{(isSpanParsable ? "" : "//")}ISpanParsable<{idTypeName}>, ISpanParsable{formattableParsableWrapperSuffix},
{(isUtf8SpanFormattable ? "" : "//")}IUtf8SpanFormattable, IUtf8SpanFormattable{formattableParsableWrapperSuffix},
{(isUtf8SpanParsable ? "" : "//")}IUtf8SpanParsable<{idTypeName}>, IUtf8SpanParsable{formattableParsableWrapperSuffix},
- ISerializableDomainObject<{idTypeName}, {underlyingTypeFullyQualifiedName}>
+ IDirectValueWrapper<{idTypeName}, {underlyingTypeFullyQualifiedName}>,
+ ICoreValueWrapper<{idTypeName}, {coreTypeFullyQualifiedName}>
{{
{(existingComponents.HasFlags(IdTypeComponents.Value) ? "/*" : "")}
{nonNullStringSummary}
@@ -545,6 +555,7 @@ namespace {containingNamespace}
{(existingComponents.HasFlags(IdTypeComponents.Constructor) ? "*/" : "")}
{(existingComponents.HasFlags(IdTypeComponents.CreateMethod) ? "/*" : "")}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
static {idTypeName} IValueWrapper<{idTypeName}, {underlyingTypeFullyQualifiedName}>.Create({underlyingTypeFullyQualifiedName} value)
{{
return new {idTypeName}(value);
@@ -555,7 +566,8 @@ namespace {containingNamespace}
///
/// Serializes a domain object as a plain value.
///
- {underlyingTypeFullyQualifiedName}{(underlyingTypeIsStruct || isNonNullString ? "" : "?")} ISerializableDomainObject<{idTypeName}, {underlyingTypeFullyQualifiedName}>.Serialize()
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ {underlyingTypeFullyQualifiedName}{(underlyingTypeIsStruct || isNonNullString ? "" : "?")} IValueWrapper<{idTypeName}, {underlyingTypeFullyQualifiedName}>.Serialize()
{{
return this.Value;
}}
@@ -565,14 +577,48 @@ namespace {containingNamespace}
///
/// Deserializes a plain value back into a domain object, without using a parameterized constructor.
///
- static {idTypeName} ISerializableDomainObject<{idTypeName}, {underlyingTypeFullyQualifiedName}>.Deserialize({underlyingTypeFullyQualifiedName} value)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static {idTypeName} IValueWrapper<{idTypeName}, {underlyingTypeFullyQualifiedName}>.Deserialize({underlyingTypeFullyQualifiedName} value)
{{
- {(existingComponents.HasFlag(IdTypeComponents.UnsettableValue) ? "// To instead get safe syntax, make the Value property '{ get; private init; }' (or let the source generator implement it)" : "")}
- {(existingComponents.HasFlag(IdTypeComponents.UnsettableValue) ? $"return System.Runtime.CompilerServices.Unsafe.As<{underlyingTypeFullyQualifiedName}, {idTypeName}>(ref value);" : "")}
- {(existingComponents.HasFlag(IdTypeComponents.UnsettableValue) ? "//" : "")}return new {idTypeName}() {{ Value = value }};
+ {(existingComponents.HasFlags(IdTypeComponents.UnsettableValue) ? "// To instead get safe syntax, make the Value property '{ get; private init; }' (or let the source generator implement it)" : "")}
+ {(existingComponents.HasFlags(IdTypeComponents.UnsettableValue) ? $"return System.Runtime.CompilerServices.Unsafe.As<{underlyingTypeFullyQualifiedName}, {idTypeName}>(ref value);" : "")}
+ {(existingComponents.HasFlags(IdTypeComponents.UnsettableValue) ? "//" : "")}return new {idTypeName}() {{ Value = value }};
}}
{(existingComponents.HasFlags(IdTypeComponents.DeserializeFromUnderlying) ? "*/" : "")}
+ {(generatable.ExistingComponents.HasFlags(IdTypeComponents.CoreValueWrapperInterface) ? "/* Core manually specified" : coreTypeFullyQualifiedName == underlyingTypeFullyQualifiedName ? "/* For nested wrapper types only" : "")}
+ [MaybeNull]
+ {coreTypeFullyQualifiedName} IValueWrapper<{idTypeName}, {coreTypeFullyQualifiedName}>.Value => ValueWrapperUnwrapper.Unwrap<{underlyingTypeFullyQualifiedName}, {coreTypeFullyQualifiedName}>(this.Value);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static {idTypeName} IValueWrapper<{idTypeName}, {coreTypeFullyQualifiedName}>.Create({coreTypeFullyQualifiedName} value)
+ {{
+ var intermediateValue = ValueWrapperUnwrapper.Wrap<{underlyingTypeFullyQualifiedName}, {coreTypeFullyQualifiedName}>(value);
+ return ValueWrapperUnwrapper.Wrap<{idTypeName}, {underlyingTypeFullyQualifiedName}>(intermediateValue);
+ }}
+
+ ///
+ /// Serializes a domain object as a plain value.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [return: MaybeNull]
+ {coreTypeFullyQualifiedName} IValueWrapper<{idTypeName}, {coreTypeFullyQualifiedName}>.Serialize()
+ {{
+ return DomainObjectSerializer.Serialize<{underlyingTypeFullyQualifiedName}, {coreTypeFullyQualifiedName}>(
+ DomainObjectSerializer.Serialize<{idTypeName}, {underlyingTypeFullyQualifiedName}>(this));
+ }}
+
+ ///
+ /// Deserializes a plain value back into a domain object, without using a parameterized constructor.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static {idTypeName} IValueWrapper<{idTypeName}, {coreTypeFullyQualifiedName}>.Deserialize({coreTypeFullyQualifiedName} value)
+ {{
+ var intermediateValue = DomainObjectSerializer.Deserialize<{underlyingTypeFullyQualifiedName}, {coreTypeFullyQualifiedName}>(value);
+ return DomainObjectSerializer.Deserialize<{idTypeName}, {underlyingTypeFullyQualifiedName}>(intermediateValue);
+ }}
+ {(coreTypeFullyQualifiedName == underlyingTypeFullyQualifiedName ? "*/" : generatable.ExistingComponents.HasFlags(IdTypeComponents.CoreValueWrapperInterface) ? "*/" : "")}
+
{(existingComponents.HasFlags(IdTypeComponents.StringComparison) ? "/*" : "")}
{(isString
? @"private StringComparison StringComparison => StringComparison.Ordinal;"
@@ -619,43 +665,21 @@ public int CompareTo({idTypeName} other)
}}
{(existingComponents.HasFlags(IdTypeComponents.CompareToMethod) ? "*/" : "")}
- {(existingComponents.HasFlags(IdTypeComponents.EqualsOperator) ? "/*" : "")}
- public static bool operator ==({idTypeName} left, {idTypeName} right) => left.Equals(right);
- {(existingComponents.HasFlags(IdTypeComponents.EqualsOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(IdTypeComponents.NotEqualsOperator) ? "/*" : "")}
- public static bool operator !=({idTypeName} left, {idTypeName} right) => !(left == right);
- {(existingComponents.HasFlags(IdTypeComponents.NotEqualsOperator) ? "*/" : "")}
-
- {(existingComponents.HasFlags(IdTypeComponents.GreaterThanOperator) ? "/*" : "")}
- public static bool operator >({idTypeName} left, {idTypeName} right) => left.CompareTo(right) > 0;
- {(existingComponents.HasFlags(IdTypeComponents.GreaterThanOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(IdTypeComponents.LessThanOperator) ? "/*" : "")}
- public static bool operator <({idTypeName} left, {idTypeName} right) => left.CompareTo(right) < 0;
- {(existingComponents.HasFlags(IdTypeComponents.LessThanOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(IdTypeComponents.GreaterEqualsOperator) ? "/*" : "")}
- public static bool operator >=({idTypeName} left, {idTypeName} right) => left.CompareTo(right) >= 0;
- {(existingComponents.HasFlags(IdTypeComponents.GreaterEqualsOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(IdTypeComponents.LessEqualsOperator) ? "/*" : "")}
- public static bool operator <=({idTypeName} left, {idTypeName} right) => left.CompareTo(right) <= 0;
- {(existingComponents.HasFlags(IdTypeComponents.LessEqualsOperator) ? "*/" : "")}
-
- {(existingComponents.HasFlags(IdTypeComponents.ConvertToOperator) ? "/*" : "")}
- public static implicit operator {idTypeName}({underlyingTypeFullyQualifiedName}{(underlyingTypeIsStruct ? "" : "?")} value) => new {idTypeName}(value);
- {(existingComponents.HasFlags(IdTypeComponents.ConvertToOperator) ? "*/" : "")}
-
- {(existingComponents.HasFlags(IdTypeComponents.ConvertFromOperator) ? "/*" : "")}{nonNullStringSummary}
- public static implicit operator {underlyingTypeFullyQualifiedName}{(underlyingTypeIsStruct || isNonNullString ? "" : "?")}({idTypeName} id) => id.Value;
- {(existingComponents.HasFlags(IdTypeComponents.ConvertFromOperator) ? "*/" : "")}
-
- {(existingComponents.HasFlags(IdTypeComponents.NullableConvertToOperator) ? "/*" : "")}
- [return: NotNullIfNotNull(""value"")]
- public static implicit operator {idTypeName}?({underlyingTypeFullyQualifiedName}? value) => value is null ? ({idTypeName}?)null : new {idTypeName}(value{(underlyingTypeIsStruct ? ".Value" : "")});
- {(existingComponents.HasFlags(IdTypeComponents.NullableConvertToOperator) ? "*/" : "")}
-
- {(existingComponents.HasFlags(IdTypeComponents.NullableConvertFromOperator) ? "/*" : "")}{nonNullStringSummary}
- {(underlyingTypeIsStruct || isNonNullString ? @"[return: NotNullIfNotNull(""id"")]" : "")}
- public static implicit operator {underlyingTypeFullyQualifiedName}?({idTypeName}? id) => id?.Value;
- {(existingComponents.HasFlags(IdTypeComponents.NullableConvertFromOperator) ? "*/" : "")}
+ {(existingComponents.HasFlags(IdTypeComponents.EqualsOperator) ? "//" : "")}public static bool operator ==({idTypeName} left, {idTypeName} right) => left.Equals(right);
+ {(existingComponents.HasFlags(IdTypeComponents.NotEqualsOperator) ? "//" : "")}public static bool operator !=({idTypeName} left, {idTypeName} right) => !(left == right);
+
+ {(existingComponents.HasFlags(IdTypeComponents.GreaterThanOperator) ? "//" : "")}public static bool operator >({idTypeName} left, {idTypeName} right) => left.CompareTo(right) > 0;
+ {(existingComponents.HasFlags(IdTypeComponents.LessThanOperator) ? "//" : "")}public static bool operator <({idTypeName} left, {idTypeName} right) => left.CompareTo(right) < 0;
+ {(existingComponents.HasFlags(IdTypeComponents.GreaterEqualsOperator) ? "//" : "")}public static bool operator >=({idTypeName} left, {idTypeName} right) => left.CompareTo(right) >= 0;
+ {(existingComponents.HasFlags(IdTypeComponents.LessEqualsOperator) ? "//" : "")}public static bool operator <=({idTypeName} left, {idTypeName} right) => left.CompareTo(right) <= 0;
+
+ {(existingComponents.HasFlags(IdTypeComponents.ConvertToOperator) ? "//" : "")}public static implicit operator {idTypeName}({underlyingTypeFullyQualifiedName}{(underlyingTypeIsStruct ? "" : "?")} value) => new {idTypeName}(value);
+ {(existingComponents.HasFlags(IdTypeComponents.ConvertFromOperator) ? "//" : "")}public static implicit operator {underlyingTypeFullyQualifiedName}{(underlyingTypeIsStruct || isNonNullString ? "" : "?")}({idTypeName} id) => id.Value;
+
+ {(existingComponents.HasFlags(IdTypeComponents.NullableConvertToOperator) ? "//" : "")}[return: NotNullIfNotNull(""value"")]
+ {(existingComponents.HasFlags(IdTypeComponents.NullableConvertToOperator) ? "//" : "")}public static implicit operator {idTypeName}?({underlyingTypeFullyQualifiedName}? value) => value is null ? ({idTypeName}?)null : new {idTypeName}(value{(underlyingTypeIsStruct ? ".Value" : "")});
+ {(existingComponents.HasFlags(IdTypeComponents.NullableConvertFromOperator) ? "//" : "")}{(underlyingTypeIsStruct || isNonNullString ? @"[return: NotNullIfNotNull(""id"")]" : "[return: MaybeNull]")}
+ {(existingComponents.HasFlags(IdTypeComponents.NullableConvertFromOperator) ? "//" : "")}public static implicit operator {underlyingTypeFullyQualifiedName}?({idTypeName}? id) => id?.Value;
#region Formatting & Parsing
@@ -760,6 +784,8 @@ internal enum IdTypeComponents : ulong
Utf8SpanParsableTryParseMethod = 1UL << 31,
Utf8SpanParsableParseMethod = 1UL << 32,
CreateMethod = 1UL << 33,
+ DirectValueWrapperInterface = 1UL << 34,
+ CoreValueWrapperInterface = 1UL << 35,
}
private sealed record Generatable
@@ -787,10 +813,8 @@ private sealed record Generatable
public bool UnderlyingTypeIsNonNullString { get => this._bits.GetBit(11); set => this._bits.SetBit(11, value); }
public bool UnderlyingTypeIsNumericUnsuitableForJson { get => this._bits.GetBit(12); set => this._bits.SetBit(12, value); }
public bool UnderlyingTypeIsStruct { get => this._bits.GetBit(13); set => this._bits.SetBit(13, value); }
- public bool IsSerializableDomainObject { get => this._bits.GetBit(14); set => this._bits.SetBit(14, value); }
public Accessibility Accessibility { get; set; }
public IdTypeComponents ExistingComponents { get; set; }
- public SimpleLocation? EntityTypeLocation { get; set; }
- public SimpleLocation? IdTypeLocation { get; set; }
+ public Diagnostic? Problem { get; set; }
}
}
diff --git a/DomainModeling.Generator/SymbolExtensions.cs b/DomainModeling.Generator/SymbolExtensions.cs
index 6c571b4..2da6033 100644
--- a/DomainModeling.Generator/SymbolExtensions.cs
+++ b/DomainModeling.Generator/SymbolExtensions.cs
@@ -15,14 +15,11 @@ public static bool HasNameOrExplicitInterfaceImplementationName(this ISymbol sym
var index = haystack.LastIndexOf(needle);
- if (index < 0)
- return false;
-
- if (index == 0)
- return true;
-
- var nameFollowsDot = haystack[index - 1] == '.';
-
- return nameFollowsDot;
+ return index switch
+ {
+ < 0 => false, // Name not found
+ 0 => haystack.Length == needle.Length, // Starts with name, so depends on whether is exact match
+ _ => haystack[index - 1] == '.' && haystack.Length == index + needle.Length, // Contains name, so depends on whether name directly follows a dot and is suffix
+ };
}
}
diff --git a/DomainModeling.Generator/TypeSymbolExtensions.cs b/DomainModeling.Generator/TypeSymbolExtensions.cs
index c12e068..d7ede56 100644
--- a/DomainModeling.Generator/TypeSymbolExtensions.cs
+++ b/DomainModeling.Generator/TypeSymbolExtensions.cs
@@ -11,8 +11,6 @@ internal static class TypeSymbolExtensions
{
private const string ComparisonsNamespace = "Architect.DomainModeling.Comparisons";
- private static IReadOnlyCollection ConversionOperatorNames { get; } = ["op_Implicit", "op_Explicit",];
-
///
/// Returns the full CLR metadata name of the , e.g. "Namespace.Type+NestedGenericType`1".
///
@@ -554,7 +552,7 @@ public static bool HasEqualsOverride(this ITypeSymbol typeSymbol)
public static bool HasConversionTo(this ITypeSymbol typeSymbol, SpecialType specialType)
{
var result = typeSymbol.SpecialType != specialType && typeSymbol.GetMembers().Any(member =>
- member is IMethodSymbol method && ConversionOperatorNames.Contains(method.Name) && member.DeclaredAccessibility == Accessibility.Public &&
+ member is IMethodSymbol { Name: WellKnownMemberNames.ExplicitConversionName or WellKnownMemberNames.ImplicitConversionName, DeclaredAccessibility: Accessibility.Public, } method &&
method.ReturnType.SpecialType == specialType);
return result;
}
@@ -565,8 +563,8 @@ public static bool HasConversionTo(this ITypeSymbol typeSymbol, SpecialType spec
public static bool HasConversionFrom(this ITypeSymbol typeSymbol, SpecialType specialType)
{
var result = typeSymbol.SpecialType != specialType && typeSymbol.GetMembers().Any(member =>
- member is IMethodSymbol method && ConversionOperatorNames.Contains(method.Name) && member.DeclaredAccessibility == Accessibility.Public &&
- method.Parameters.Length == 1 && method.Parameters[0].Type.SpecialType == specialType);
+ member is IMethodSymbol { Name: WellKnownMemberNames.ExplicitConversionName or WellKnownMemberNames.ImplicitConversionName, DeclaredAccessibility: Accessibility.Public, Parameters.Length: 1, } method &&
+ method.Parameters[0].Type.SpecialType == specialType);
return result;
}
diff --git a/DomainModeling.Generator/ValueObjectGenerator.cs b/DomainModeling.Generator/ValueObjectGenerator.cs
index 471b112..2b04072 100644
--- a/DomainModeling.Generator/ValueObjectGenerator.cs
+++ b/DomainModeling.Generator/ValueObjectGenerator.cs
@@ -67,55 +67,55 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
// Records override this, but our implementation is superior
existingComponents |= ValueObjectTypeComponents.ToStringOverride.If(members.Any(member =>
- member.Name == nameof(ToString) && member is IMethodSymbol { IsImplicitlyDeclared: false } method && method.Parameters.Length == 0));
+ member is IMethodSymbol { Name: nameof(ToString), IsImplicitlyDeclared: false, IsOverride: true, Arity: 0, Parameters.Length: 0, }));
// Records override this, but our implementation is superior
existingComponents |= ValueObjectTypeComponents.GetHashCodeOverride.If(members.Any(member =>
- member.Name == nameof(GetHashCode) && member is IMethodSymbol { IsImplicitlyDeclared: false } method && method.Parameters.Length == 0));
+ member is IMethodSymbol { Name: nameof(GetHashCode), IsImplicitlyDeclared: false, IsOverride: true, Arity: 0, Parameters.Length: 0, }));
// Records irrevocably and correctly override this, checking the type and delegating to IEquatable.Equals(T)
existingComponents |= ValueObjectTypeComponents.EqualsOverride.If(members.Any(member =>
- member.Name == nameof(Equals) && member is IMethodSymbol method && method.Parameters.Length == 1 &&
+ member is IMethodSymbol { Name: nameof(Equals), IsOverride: true, Arity: 0, Parameters.Length: 1, } method &&
method.Parameters[0].Type.SpecialType == SpecialType.System_Object));
// Records override this, but our implementation is superior
existingComponents |= ValueObjectTypeComponents.EqualsMethod.If(members.Any(member =>
- member.Name == nameof(Equals) && member is IMethodSymbol { IsImplicitlyDeclared: false } method && method.Parameters.Length == 1 &&
+ member.HasNameOrExplicitInterfaceImplementationName(nameof(Equals)) && member is IMethodSymbol { IsImplicitlyDeclared: false, IsOverride: false, Arity: 0, Parameters.Length: 1, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= ValueObjectTypeComponents.CompareToMethod.If(members.Any(member =>
- member.Name == nameof(IComparable.CompareTo) && member is IMethodSymbol method && method.Parameters.Length == 1 &&
+ member.HasNameOrExplicitInterfaceImplementationName(nameof(IComparable.CompareTo)) && member is IMethodSymbol { Arity: 0, Parameters.Length: 1, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default)));
// Records irrevocably and correctly override this, delegating to IEquatable.Equals(T)
existingComponents |= ValueObjectTypeComponents.EqualsOperator.If(members.Any(member =>
- member.Name == "op_Equality" && member is IMethodSymbol method && method.Parameters.Length == 2 &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.EqualityOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
// Records irrevocably and correctly override this, delegating to IEquatable.Equals(T)
existingComponents |= ValueObjectTypeComponents.NotEqualsOperator.If(members.Any(member =>
- member.Name == "op_Inequality" && member is IMethodSymbol method && method.Parameters.Length == 2 &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.InequalityOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= ValueObjectTypeComponents.GreaterThanOperator.If(members.Any(member =>
- member.Name == "op_GreaterThan" && member is IMethodSymbol method && method.Parameters.Length == 2 &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.GreaterThanOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= ValueObjectTypeComponents.LessThanOperator.If(members.Any(member =>
- member.Name == "op_LessThan" && member is IMethodSymbol method && method.Parameters.Length == 2 &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.LessThanOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= ValueObjectTypeComponents.GreaterEqualsOperator.If(members.Any(member =>
- member.Name == "op_GreaterThanOrEqual" && member is IMethodSymbol method && method.Parameters.Length == 2 &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.GreaterThanOrEqualOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= ValueObjectTypeComponents.LessEqualsOperator.If(members.Any(member =>
- member.Name == "op_LessThanOrEqual" && member is IMethodSymbol method && method.Parameters.Length == 2 &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.LessThanOrEqualOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
@@ -271,11 +271,9 @@ namespace {containingNamespace}
{{
/* Generated */ {type.DeclaredAccessibility.ToCodeString()} sealed partial{(isRecord ? " record" : "")} class {typeName} : ValueObject, IEquatable<{typeName}>{(isComparable ? "" : "/*")}, IComparable<{typeName}>{(isComparable ? "" : "*/")}
{{
- {(isRecord || existingComponents.HasFlags(ValueObjectTypeComponents.StringComparison) ? "/*" : "")}
- {(dataMembers.Any(member => member.Type.SpecialType == SpecialType.System_String)
+ {(isRecord || existingComponents.HasFlags(ValueObjectTypeComponents.StringComparison) ? "//" : "")}{(dataMembers.Any(member => member.Type.SpecialType == SpecialType.System_String)
? @"protected sealed override StringComparison StringComparison => StringComparison.Ordinal;"
: @"protected sealed override StringComparison StringComparison => throw new NotSupportedException(""This operation applies to string-based value objects only."");")}
- {(isRecord || existingComponents.HasFlags(ValueObjectTypeComponents.StringComparison) ? "*/" : "")}
{(existingComponents.HasFlags(ValueObjectTypeComponents.DefaultConstructor) ? "/*" : "")}
#pragma warning disable CS8618 // Deserialization constructor
@@ -350,26 +348,14 @@ private static int Compare(T left, T right)
{(isComparable ? "" : "*/")}
{(existingComponents.HasFlags(ValueObjectTypeComponents.CompareToMethod) ? "*/" : "")}
- {(existingComponents.HasFlags(ValueObjectTypeComponents.EqualsOperator) ? "/*" : "")}
- public static bool operator ==({typeName}? left, {typeName}? right) => left is null ? right is null : left.Equals(right);
- {(existingComponents.HasFlags(ValueObjectTypeComponents.EqualsOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(ValueObjectTypeComponents.NotEqualsOperator) ? "/*" : "")}
- public static bool operator !=({typeName}? left, {typeName}? right) => !(left == right);
- {(existingComponents.HasFlags(ValueObjectTypeComponents.NotEqualsOperator) ? "*/" : "")}
+ {(existingComponents.HasFlags(ValueObjectTypeComponents.EqualsOperator) ? "//" : "")}public static bool operator ==({typeName}? left, {typeName}? right) => left is null ? right is null : left.Equals(right);
+ {(existingComponents.HasFlags(ValueObjectTypeComponents.NotEqualsOperator) ? "//" : "")}public static bool operator !=({typeName}? left, {typeName}? right) => !(left == right);
{(isComparable ? "" : "/*")}
- {(existingComponents.HasFlags(ValueObjectTypeComponents.GreaterThanOperator) ? "/*" : "")}
- public static bool operator >({typeName}? left, {typeName}? right) => left is null ? false : left.CompareTo(right) > 0;
- {(existingComponents.HasFlags(ValueObjectTypeComponents.GreaterThanOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(ValueObjectTypeComponents.LessThanOperator) ? "/*" : "")}
- public static bool operator <({typeName}? left, {typeName}? right) => left is null ? right is not null : left.CompareTo(right) < 0;
- {(existingComponents.HasFlags(ValueObjectTypeComponents.LessThanOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(ValueObjectTypeComponents.GreaterEqualsOperator) ? "/*" : "")}
- public static bool operator >=({typeName}? left, {typeName}? right) => !(left < right);
- {(existingComponents.HasFlags(ValueObjectTypeComponents.GreaterEqualsOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(ValueObjectTypeComponents.LessEqualsOperator) ? "/*" : "")}
- public static bool operator <=({typeName}? left, {typeName}? right) => !(left > right);
- {(existingComponents.HasFlags(ValueObjectTypeComponents.LessEqualsOperator) ? "*/" : "")}
+ {(existingComponents.HasFlags(ValueObjectTypeComponents.GreaterThanOperator) ? "//" : "")}public static bool operator >({typeName}? left, {typeName}? right) => left is null ? false : left.CompareTo(right) > 0;
+ {(existingComponents.HasFlags(ValueObjectTypeComponents.LessThanOperator) ? "//" : "")}public static bool operator <({typeName}? left, {typeName}? right) => left is null ? right is not null : left.CompareTo(right) < 0;
+ {(existingComponents.HasFlags(ValueObjectTypeComponents.GreaterEqualsOperator) ? "//" : "")}public static bool operator >=({typeName}? left, {typeName}? right) => !(left < right);
+ {(existingComponents.HasFlags(ValueObjectTypeComponents.LessEqualsOperator) ? "//" : "")}public static bool operator <=({typeName}? left, {typeName}? right) => !(left > right);
{(isComparable ? "" : "*/")}
}}
}}
diff --git a/DomainModeling.Generator/ValueWrapperGenerator.cs b/DomainModeling.Generator/ValueWrapperGenerator.cs
index bedb089..42493ec 100644
--- a/DomainModeling.Generator/ValueWrapperGenerator.cs
+++ b/DomainModeling.Generator/ValueWrapperGenerator.cs
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
+using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis;
namespace Architect.DomainModeling.Generator;
@@ -23,43 +24,78 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var valueWrappers = identities.Combine(wrapperValueObjects)
.Select((tuple, ct) => tuple.Left.AddRange(tuple.Right));
- this.IdentityGenerator.Generate(context, identities, valueWrappers);
- this.WrapperValueObjectGenerator.Generate(context, wrapperValueObjects, valueWrappers);
+ this.IdentityGenerator.Generate(context, valueWrappers);
+ this.WrapperValueObjectGenerator.Generate(context, valueWrappers);
}
+ internal static string GetCoreTypeFullyQualifiedName(
+ ImmutableArray valueWrappers,
+ string typeName, string containingNamespace)
+ {
+ // Concatenate our namespace and name, so that we are similar to further iterations
+ Span initialFullyQualifiedTypeName = stackalloc char[containingNamespace.Length + 1 + typeName.Length];
+ initialFullyQualifiedTypeName = [.. containingNamespace, '.', .. typeName];
+
+ var result = (string?)null;
+
+ var nextTypeName = (ReadOnlySpan)initialFullyQualifiedTypeName;
+ bool couldDigDeeper;
+ do
+ {
+ couldDigDeeper = false;
+ foreach (ref readonly var item in valueWrappers.AsSpan())
+ {
+ // Based on the fully qualified type name we are looking for, try to find the corresponding generatable
+ if (item.ContainingNamespace.Length + 1 + item.TypeName.Length == nextTypeName.Length &&
+ nextTypeName.EndsWith(item.TypeName.AsSpan()) &&
+ nextTypeName.StartsWith(item.ContainingNamespace.AsSpan()) &&
+ nextTypeName[item.ContainingNamespace.Length] == '.')
+ {
+ couldDigDeeper = true;
+ result = item.CustomCoreTypeFullyQualifiedName ?? item.UnderlyingTypeFullyQualifiedName;
+ nextTypeName = result.AsSpan();
+ break;
+ }
+ }
+ } while (couldDigDeeper);
+
+ return result ?? initialFullyQualifiedTypeName.ToString();
+ }
+
+ // ATTENTION: This method cannot be combined with the other recursive one, because this one's results are affected by intermediate items, not just the deepest item
///
/// Utility method that recursively determines which formatting and parsing interfaces are supported, based on all known value wrappers.
/// This allows even nested value wrappers to dig down into the deepest underlying type.
///
internal static (bool isSpanFormattable, bool isSpanParsable, bool isUtf8SpanFormattable, bool isUtf8SpanParsable) GetFormattabilityAndParsabilityRecursively(
- ImmutableArray valueWrapperGeneratables,
- string typeName, string containingNamespace, string underlyingTypeFullyQualifiedName)
+ ImmutableArray valueWrappers,
+ string typeName, string containingNamespace)
{
var isSpanFormattable = false;
var isSpanParsable = false;
var isUtf8SpanFormattable = false;
var isUtf8SpanParsable = false;
- // Concatenate our namespace and name, so that we are comparable to further iterations
- Span ownFullyQualifiedTypeName = stackalloc char[containingNamespace.Length + 1 + typeName.Length];
- ownFullyQualifiedTypeName = [.. containingNamespace, '.', .. typeName];
+ // Concatenate our namespace and name, so that we are similar to further iterations
+ Span initialFullyQualifiedTypeName = stackalloc char[containingNamespace.Length + 1 + typeName.Length];
+ initialFullyQualifiedTypeName = [.. containingNamespace, '.', .. typeName];
// A generated type will honor the formattability/parsability of its underlying type
// As such, for generated types, it is worth recursing into the underlying types to discover if formattability/parsability is available through the chain
- var nextTypeName = (ReadOnlySpan)ownFullyQualifiedTypeName;
- bool hasUnderlyingGeneratedType;
+ var nextTypeName = (ReadOnlySpan)initialFullyQualifiedTypeName;
+ bool couldDigDeeper;
do
{
- hasUnderlyingGeneratedType = false;
- foreach (ref readonly var item in valueWrapperGeneratables.AsSpan())
+ couldDigDeeper = false;
+ foreach (ref readonly var item in valueWrappers.AsSpan())
{
- // Based on the fully qualified type name we are looking for, try to find the corresponding NameOnlyGeneratable
- if (nextTypeName.EndsWith(item.TypeName.AsSpan()) &&
+ // Based on the fully qualified type name we are looking for, try to find the corresponding generatable
+ if (item.ContainingNamespace.Length + 1 + item.TypeName.Length == nextTypeName.Length &&
+ nextTypeName.EndsWith(item.TypeName.AsSpan()) &&
nextTypeName.StartsWith(item.ContainingNamespace.AsSpan()) &&
- item.ContainingNamespace.Length + 1 + item.TypeName.Length == nextTypeName.Length &&
nextTypeName[item.ContainingNamespace.Length] == '.')
{
- hasUnderlyingGeneratedType = true;
+ couldDigDeeper = true;
nextTypeName = item.UnderlyingTypeFullyQualifiedName.AsSpan();
isSpanFormattable |= item.IsSpanFormattable;
isSpanParsable |= item.IsSpanParsable;
@@ -68,33 +104,44 @@ internal static (bool isSpanFormattable, bool isSpanParsable, bool isUtf8SpanFor
break;
}
}
- } while (hasUnderlyingGeneratedType && (isSpanFormattable & isSpanParsable & isUtf8SpanFormattable & isUtf8SpanParsable) == false); // Possible & worth seeking deeper
+ } while (couldDigDeeper && (isSpanFormattable & isSpanParsable & isUtf8SpanFormattable & isUtf8SpanParsable) == false); // Possible & worth seeking deeper
return (isSpanFormattable, isSpanParsable, isUtf8SpanFormattable, isUtf8SpanParsable);
}
+ [StructLayout(LayoutKind.Auto)]
internal readonly record struct BasicGeneratable
{
+ public bool IsIdentity { get; }
public string TypeName { get; }
public string ContainingNamespace { get; }
public string UnderlyingTypeFullyQualifiedName { get; }
+ ///
+ /// Set only if manually chosen by the developer.
+ /// Helps implement wrappers around unofficial wrapper type, such as a WrapperValueObject<Uri> that pretends its core type is string.
+ ///
+ public string? CustomCoreTypeFullyQualifiedName { get; }
public bool IsSpanFormattable { get; }
public bool IsSpanParsable { get; }
public bool IsUtf8SpanFormattable { get; }
public bool IsUtf8SpanParsable { get; }
public BasicGeneratable(
+ bool isIdentity,
string typeName,
string containingNamespace,
string underlyingTypeFullyQualifiedName,
+ string? customCoreTypeFullyQualifiedName,
bool isSpanFormattable,
bool isSpanParsable,
bool isUtf8SpanFormattable,
bool isUtf8SpanParsable)
{
+ this.IsIdentity = isIdentity;
this.TypeName = typeName;
this.ContainingNamespace = containingNamespace;
this.UnderlyingTypeFullyQualifiedName = underlyingTypeFullyQualifiedName;
+ this.CustomCoreTypeFullyQualifiedName = customCoreTypeFullyQualifiedName;
this.IsSpanFormattable = isSpanFormattable;
this.IsSpanParsable = isSpanParsable;
this.IsUtf8SpanFormattable = isUtf8SpanFormattable;
diff --git a/DomainModeling.Generator/WrapperValueObjectGenerator.cs b/DomainModeling.Generator/WrapperValueObjectGenerator.cs
index b8c1b14..5986ee8 100644
--- a/DomainModeling.Generator/WrapperValueObjectGenerator.cs
+++ b/DomainModeling.Generator/WrapperValueObjectGenerator.cs
@@ -27,18 +27,22 @@ internal void InitializeBasicProvider(IncrementalGeneratorInitializationContext
(context, ct) => context.SemanticModel.GetDeclaredSymbol((TypeDeclarationSyntax)context.Node) switch
{
INamedTypeSymbol type when HasRequiredAttribute(type, out var attribute) && attribute.AttributeClass!.TypeArguments[0] is ITypeSymbol underlyingType =>
- new ValueWrapperGenerator.BasicGeneratable(
- typeName: type.Name,
- containingNamespace: type.ContainingNamespace.ToString(),
- underlyingTypeFullyQualifiedName: underlyingType.ToString() is string underlyingTypeName ? underlyingTypeName : (underlyingTypeName = null!),
- isSpanFormattable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
- interf is { Name: "ISpanFormattable", ContainingNamespace.Name: "System", Arity: 0, }),
- isSpanParsable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
- interf is { Name: "ISpanParsable", ContainingNamespace.Name: "System", Arity: 1, }),
- isUtf8SpanFormattable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
- interf is { Name: "IUtf8SpanFormattable", ContainingNamespace.Name: "System", Arity: 0, }),
- isUtf8SpanParsable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
- interf is { Name: "IUtf8SpanParsable", ContainingNamespace.Name: "System", Arity: 1, })),
+ GetFirstProblem((TypeDeclarationSyntax)context.Node, type, underlyingType) is { }
+ ? default
+ : new ValueWrapperGenerator.BasicGeneratable(
+ isIdentity: false,
+ typeName: type.Name,
+ containingNamespace: type.ContainingNamespace.ToString(),
+ underlyingTypeFullyQualifiedName: underlyingType.ToString() is string underlyingTypeName ? underlyingTypeName : (underlyingTypeName = null!),
+ customCoreTypeFullyQualifiedName: type.AllInterfaces.FirstOrDefault(interf => interf.IsType("ICoreValueWrapper", "Architect", "DomainModeling", arity: 2))?.TypeArguments[1].ToString(),
+ isSpanFormattable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
+ interf is { Name: "ISpanFormattable", ContainingNamespace.Name: "System", Arity: 0, }),
+ isSpanParsable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
+ interf is { Name: "ISpanParsable", ContainingNamespace.Name: "System", Arity: 1, }),
+ isUtf8SpanFormattable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
+ interf is { Name: "IUtf8SpanFormattable", ContainingNamespace.Name: "System", Arity: 0, }),
+ isUtf8SpanParsable: underlyingType.SpecialType == SpecialType.System_String || underlyingType.AllInterfaces.Any(interf =>
+ interf is { Name: "IUtf8SpanParsable", ContainingNamespace.Name: "System", Arity: 1, })),
_ => default,
})
.Where(generatable => generatable != default)
@@ -50,8 +54,8 @@ INamedTypeSymbol type when HasRequiredAttribute(type, out var attribute) && attr
/// Additionally gathers detailed info per individual wrapper value object.
/// Generates source based on all of the above.
///
- internal void Generate(IncrementalGeneratorInitializationContext context,
- IncrementalValueProvider> wrapperValueObjects,
+ internal void Generate(
+ IncrementalGeneratorInitializationContext context,
IncrementalValueProvider> valueWrappers)
{
var provider = context.SyntaxProvider.CreateSyntaxProvider(FilterSyntaxNode, TransformSyntaxNode)
@@ -60,7 +64,7 @@ internal void Generate(IncrementalGeneratorInitializationContext context,
context.RegisterSourceOutput(provider.Combine(valueWrappers), GenerateSource!);
- var aggregatedProvider = wrapperValueObjects.Combine(EntityFrameworkConfigurationGenerator.CreateMetadataProvider(context));
+ var aggregatedProvider = valueWrappers.Combine(EntityFrameworkConfigurationGenerator.CreateMetadataProvider(context));
context.RegisterSourceOutput(aggregatedProvider, DomainModelConfiguratorGenerator.GenerateSourceForWrapperValueObjects);
}
@@ -73,6 +77,70 @@ private static bool HasRequiredAttribute(INamedTypeSymbol type, out AttributeDat
return attribute != null;
}
+ private static Diagnostic? GetFirstProblem(TypeDeclarationSyntax tds, INamedTypeSymbol type, ITypeSymbol underlyingType)
+ {
+ var isPartial = tds.Modifiers.Any(SyntaxKind.PartialKeyword);
+
+ // Require the expected inheritance
+ if (!isPartial && !type.IsOrImplementsInterface(type => type.IsType("IWrapperValueObject", "Architect", "DomainModeling", arity: 1), out _))
+ return CreateDiagnostic("WrapperValueObjectGeneratorUnexpectedInheritance", "Unexpected inheritance",
+ "Type marked as wrapper value object lacks IWrapperValueObject interface. Did you forget the 'partial' keyword and elude source generation?", DiagnosticSeverity.Warning);
+
+ // Require IDirectValueWrapper
+ var hasDirectValueWrapperInterface = type.AllInterfaces.Any(interf =>
+ interf.IsType("IDirectValueWrapper", "Architect", "DomainModeling", arity: 2) && !interf.IsImplicitlyDeclared &&
+ interf.TypeArguments[0].Equals(type, SymbolEqualityComparer.Default) && interf.TypeArguments[1].Equals(underlyingType, SymbolEqualityComparer.Default));
+ if (!isPartial && !hasDirectValueWrapperInterface)
+ return CreateDiagnostic("WrapperValueObjectGeneratorMissingDirectValueWrapper", "Missing interface",
+ $"Type marked as identity value object lacks IDirectValueWrapper<{type.Name}, {underlyingType.Name}> interface.", DiagnosticSeverity.Warning);
+
+ // Require ICoreValueWrapper
+ var hasCoreValueWrapperInterface = type.AllInterfaces.Any(interf =>
+ interf.IsType("ICoreValueWrapper", "Architect", "DomainModeling", arity: 2) && !interf.IsImplicitlyDeclared &&
+ interf.TypeArguments[0].Equals(type, SymbolEqualityComparer.Default));
+ if (!isPartial && !hasCoreValueWrapperInterface)
+ return CreateDiagnostic("WrapperValueObjectGeneratorMissingCoreValueWrapper", "Missing interface",
+ $"Type marked as identity value object lacks ICoreValueWrapper<{type.Name}, {underlyingType.Name}> interface.", DiagnosticSeverity.Warning);
+
+ if (isPartial)
+ {
+ // Only if class
+ if (tds is not ClassDeclarationSyntax)
+ return CreateDiagnostic("WrapperValueObjectGeneratorValueType", "Source-generated struct wrapper value object",
+ "The type was not source-generated because it is a struct, while a class was expected. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning);
+
+ // Only if non-record
+ if (type.IsRecord)
+ return CreateDiagnostic("WrapperValueObjectGeneratorRecordType", "Source-generated record wrapper value object",
+ "The type was not source-generated because it is a record, which cannot inherit from a non-record base class. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning);
+
+ // Only if non-abstract
+ if (type.IsAbstract)
+ return CreateDiagnostic("WrapperValueObjectGeneratorAbstractType", "Source-generated abstract type",
+ "The type was not source-generated because it is abstract. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning);
+
+ // Only if non-generic
+ if (type.IsGeneric())
+ return CreateDiagnostic("WrapperValueObjectGeneratorGenericType", "Source-generated generic type",
+ "The type was not source-generated because it is generic. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning);
+
+ // Only if non-nested
+ if (type.IsNested())
+ return CreateDiagnostic("WrapperValueObjectGeneratorNestedType", "Source-generated nested type",
+ "The type was not source-generated because it is a nested type. To get source generation, avoid nesting it inside another type. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning);
+ }
+
+ return null;
+
+ // Local shorthand to create a diagnostic
+ Diagnostic CreateDiagnostic(string id, string title, string description, DiagnosticSeverity severity)
+ {
+ return Diagnostic.Create(
+ new DiagnosticDescriptor(id, title, description, "Architect.DomainModeling", severity, isEnabledByDefault: true),
+ type.Locations.FirstOrDefault());
+ }
+ }
+
private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancellationToken = default)
{
// Struct or class or record
@@ -104,9 +172,7 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
var underlyingType = attribute.AttributeClass!.TypeArguments[0];
var result = new Generatable();
- result.TypeLocation = type.Locations.FirstOrDefault();
result.IsWrapperValueObject = type.IsOrImplementsInterface(type => type.IsType("IWrapperValueObject", "Architect", "DomainModeling", arity: 1), out _);
- result.IsSerializableDomainObject = type.IsOrImplementsInterface(type => type.IsType("ISerializableDomainObject", "Architect", "DomainModeling", arity: 2), out _);
result.IsPartial = tds.Modifiers.Any(SyntaxKind.PartialKeyword);
result.IsRecord = type.IsRecord;
result.IsClass = type.TypeKind == TypeKind.Class;
@@ -128,8 +194,7 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
result.UnderlyingTypeIsNullable = underlyingType.IsNullable();
result.UnderlyingTypeIsString = underlyingType.SpecialType == SpecialType.System_String;
- result.ValueFieldName = type.GetMembers().FirstOrDefault(member => member is IFieldSymbol field && (field.Name == "k__BackingField" || field.Name.Equals("value") || field.Name.Equals("_value")))?.Name ??
- "_value";
+ result.ValueFieldName = type.GetMembers().FirstOrDefault(member => member is IFieldSymbol { Name: "k__BackingField" or "value" or "_value" })?.Name ?? "_value";
// IComparable is implemented on-demand, if the type implements IComparable against itself and the underlying type is self-comparable
// It is also implemented if the underlying type is an annotated identity
result.IsComparable = type.AllInterfaces.Any(interf => interf.IsSystemType("IComparable", arity: 1) && interf.TypeArguments[0].Equals(type, SymbolEqualityComparer.Default)) &&
@@ -152,98 +217,88 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
// Records override this, but our implementation is superior
existingComponents |= WrapperValueObjectTypeComponents.ToStringOverride.If(members.Any(member =>
- member.Name == nameof(ToString) && member is IMethodSymbol { IsImplicitlyDeclared: false } method && method.Parameters.Length == 0));
+ member is IMethodSymbol { Name: nameof(ToString), IsImplicitlyDeclared: false, IsOverride: true, Parameters.Length: 0, }));
// Records override this, but our implementation is superior
existingComponents |= WrapperValueObjectTypeComponents.GetHashCodeOverride.If(members.Any(member =>
- member.Name == nameof(GetHashCode) && member is IMethodSymbol { IsImplicitlyDeclared: false } method && method.Parameters.Length == 0));
+ member is IMethodSymbol { Name: nameof(GetHashCode), IsImplicitlyDeclared: false, IsOverride: true, Parameters.Length: 0, }));
// Records irrevocably and correctly override this, checking the type and delegating to IEquatable.Equals(T)
existingComponents |= WrapperValueObjectTypeComponents.EqualsOverride.If(members.Any(member =>
- member.Name == nameof(Equals) && member is IMethodSymbol method && method.Parameters.Length == 1 &&
+ member is IMethodSymbol { Name: nameof(Equals), IsOverride: true, Parameters.Length: 1, } method &&
method.Parameters[0].Type.SpecialType == SpecialType.System_Object));
// Records override this, but our implementation is superior
existingComponents |= WrapperValueObjectTypeComponents.EqualsMethod.If(members.Any(member =>
- member.Name == nameof(Equals) && member is IMethodSymbol { IsImplicitlyDeclared: false } method && method.Parameters.Length == 1 &&
+ member.HasNameOrExplicitInterfaceImplementationName(nameof(Equals)) && member is IMethodSymbol { IsImplicitlyDeclared: false, IsOverride: false, Parameters.Length: 1, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= WrapperValueObjectTypeComponents.CompareToMethod.If(members.Any(member =>
- member.Name == nameof(IComparable.CompareTo) && member is IMethodSymbol method && method.Parameters.Length == 1 &&
+ member.HasNameOrExplicitInterfaceImplementationName(nameof(IComparable.CompareTo)) && member is IMethodSymbol { Arity: 0, Parameters.Length: 1, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default)));
// Records irrevocably and correctly override this, delegating to IEquatable.Equals(T)
existingComponents |= WrapperValueObjectTypeComponents.EqualsOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_Equality") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.EqualityOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
// Records irrevocably and correctly override this, delegating to IEquatable.Equals(T)
existingComponents |= WrapperValueObjectTypeComponents.NotEqualsOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_Inequality") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.InequalityOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= WrapperValueObjectTypeComponents.GreaterThanOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_GreaterThan") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.GreaterThanOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= WrapperValueObjectTypeComponents.LessThanOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_LessThan") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.LessThanOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= WrapperValueObjectTypeComponents.GreaterEqualsOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_GreaterThanOrEqual") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.GreaterThanOrEqualOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= WrapperValueObjectTypeComponents.LessEqualsOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("op_LessThanOrEqual") &&
+ member is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator, Name: WellKnownMemberNames.LessThanOrEqualOperatorName, IsStatic: true, Parameters.Length: 2, } method &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[1].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= WrapperValueObjectTypeComponents.ConvertToOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Parameters.Length == 1 &&
- (member.HasNameOrExplicitInterfaceImplementationName("op_Implicit") || member.HasNameOrExplicitInterfaceImplementationName("op_Explicit")) &&
+ member is IMethodSymbol { MethodKind: MethodKind.Conversion, IsStatic: true, Parameters.Length: 1, } method &&
method.ReturnType.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[0].Type.Equals(underlyingType, SymbolEqualityComparer.Default)));
existingComponents |= WrapperValueObjectTypeComponents.ConvertFromOperator.If(members.Any(member =>
- member is IMethodSymbol method && method.Parameters.Length == 1 &&
- (member.HasNameOrExplicitInterfaceImplementationName("op_Implicit") || member.HasNameOrExplicitInterfaceImplementationName("op_Explicit")) &&
+ member is IMethodSymbol { MethodKind: MethodKind.Conversion, IsStatic: true, Parameters.Length: 1, } method &&
method.ReturnType.Equals(underlyingType, SymbolEqualityComparer.Default) &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default)));
// Consider having a reference-typed underlying type as already having the operator (though actually it does not apply at all)
existingComponents |= WrapperValueObjectTypeComponents.NullableConvertToOperator.If(!underlyingType.IsValueType || members.Any(member =>
- member is IMethodSymbol method && method.Parameters.Length == 1 &&
- (member.HasNameOrExplicitInterfaceImplementationName("op_Implicit") || member.HasNameOrExplicitInterfaceImplementationName("op_Explicit")) &&
+ member is IMethodSymbol { MethodKind: MethodKind.Conversion, IsStatic: true, Parameters.Length: 1, } method &&
method.ReturnType.Equals(type, SymbolEqualityComparer.Default) &&
method.Parameters[0].Type.IsNullableOf(underlyingType)));
// Consider having a reference-typed underlying type as already having the operator (though actually it does not apply at all)
existingComponents |= WrapperValueObjectTypeComponents.NullableConvertFromOperator.If(!underlyingType.IsValueType || members.Any(member =>
- member is IMethodSymbol method && method.Parameters.Length == 1 &&
- (member.HasNameOrExplicitInterfaceImplementationName("op_Implicit") || member.HasNameOrExplicitInterfaceImplementationName("op_Explicit")) &&
+ member is IMethodSymbol { MethodKind: MethodKind.Conversion, IsStatic: true, Parameters.Length: 1, } method &&
method.ReturnType.IsNullableOf(underlyingType) &&
method.Parameters[0].Type.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= WrapperValueObjectTypeComponents.SerializeToUnderlying.If(members.Any(member =>
- member.HasNameOrExplicitInterfaceImplementationName("Serialize") && member is IMethodSymbol method && method.Parameters.Length == 0 &&
- method.Arity == 0));
+ member.HasNameOrExplicitInterfaceImplementationName("Serialize") && member is IMethodSymbol { Arity: 0, IsStatic: false, Parameters.Length: 0, } method &&
+ method.ReturnType.Equals(underlyingType, SymbolEqualityComparer.Default)));
existingComponents |= WrapperValueObjectTypeComponents.DeserializeFromUnderlying.If(members.Any(member =>
- member.HasNameOrExplicitInterfaceImplementationName("Deserialize") && member is IMethodSymbol method && method.Parameters.Length == 1 &&
+ member.HasNameOrExplicitInterfaceImplementationName("Deserialize") && member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 1, } method &&
method.Parameters[0].Type.Equals(underlyingType, SymbolEqualityComparer.Default) &&
- method.Arity == 0));
+ method.ReturnType.Equals(type, SymbolEqualityComparer.Default)));
existingComponents |= WrapperValueObjectTypeComponents.SystemTextJsonConverter.If(type.GetAttributes().Any(attribute =>
attribute.AttributeClass?.IsTypeWithNamespace("JsonConverterAttribute", "System.Text.Json.Serialization") == true));
@@ -254,75 +309,75 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
existingComponents |= WrapperValueObjectTypeComponents.StringComparison.If(members.Any(member =>
member.Name == "StringComparison" && member.IsOverride));
- existingComponents |= WrapperValueObjectTypeComponents.FormattableToStringOverride.If(
- members.Any(member =>
- member.Name == nameof(IFormattable.ToString) && member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
- method.Parameters[0].Type.SpecialType == SpecialType.System_String && method.Parameters[1].Type.IsSystemType("IFormatProvider")));
-
- existingComponents |= WrapperValueObjectTypeComponents.ParsableTryParseMethod.If(
- members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 3 &&
- member.HasNameOrExplicitInterfaceImplementationName("TryParse") &&
- method.Parameters[0].Type.SpecialType == SpecialType.System_String && method.Parameters[1].Type.IsSystemType("IFormatProvider") && method.Parameters[2].Type.Equals(type, SymbolEqualityComparer.Default) && method.Parameters[2].RefKind == RefKind.Out));
-
- existingComponents |= WrapperValueObjectTypeComponents.ParsableParseMethod.If(
- members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("Parse") &&
- method.Parameters[0].Type.SpecialType == SpecialType.System_String && method.Parameters[1].Type.IsSystemType("IFormatProvider")));
-
- existingComponents |= WrapperValueObjectTypeComponents.SpanFormattableTryFormatMethod.If(
- members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 4 &&
- member.HasNameOrExplicitInterfaceImplementationName("TryFormat") &&
- method.Parameters[0].Type.IsSpanOfSpecialType(SpecialType.System_Char) &&
- method.Parameters[1].Type.SpecialType == SpecialType.System_Int32 && method.Parameters[1].RefKind == RefKind.Out &&
- method.Parameters[2].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Char) &&
- method.Parameters[3].Type.IsSystemType("IFormatProvider")));
-
- existingComponents |= WrapperValueObjectTypeComponents.SpanParsableTryParseMethod.If(
- members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 3 &&
- member.HasNameOrExplicitInterfaceImplementationName("TryParse") &&
- method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Char) &&
- method.Parameters[1].Type.IsSystemType("IFormatProvider") &&
- method.Parameters[2].Type.Equals(type, SymbolEqualityComparer.Default) && method.Parameters[2].RefKind == RefKind.Out));
-
- existingComponents |= WrapperValueObjectTypeComponents.SpanParsableParseMethod.If(
- members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("Parse") &&
- method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Char) &&
- method.Parameters[1].Type.IsSystemType("IFormatProvider")));
-
- existingComponents |= WrapperValueObjectTypeComponents.Utf8SpanFormattableTryFormatMethod.If(
- members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 4 &&
- member.HasNameOrExplicitInterfaceImplementationName("TryFormat") &&
- method.Parameters[0].Type.IsSpanOfSpecialType(SpecialType.System_Byte) &&
- method.Parameters[1].Type.SpecialType == SpecialType.System_Int32 && method.Parameters[1].RefKind == RefKind.Out &&
- method.Parameters[2].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Char) &&
- method.Parameters[3].Type.IsSystemType("IFormatProvider")));
-
- existingComponents |= WrapperValueObjectTypeComponents.Utf8SpanParsableTryParseMethod.If(
- members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 3 &&
- member.HasNameOrExplicitInterfaceImplementationName("TryParse") &&
- method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Byte) &&
- method.Parameters[1].Type.IsSystemType("IFormatProvider") &&
- method.Parameters[2].Type.Equals(type, SymbolEqualityComparer.Default) && method.Parameters[2].RefKind == RefKind.Out));
-
- existingComponents |= WrapperValueObjectTypeComponents.Utf8SpanParsableParseMethod.If(
- members.Any(member =>
- member is IMethodSymbol method && method.Arity == 0 && method.Parameters.Length == 2 &&
- member.HasNameOrExplicitInterfaceImplementationName("Parse") &&
- method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Byte) &&
- method.Parameters[1].Type.IsSystemType("IFormatProvider")));
+ existingComponents |= WrapperValueObjectTypeComponents.FormattableToStringOverride.If(members.Any(member =>
+ member.HasNameOrExplicitInterfaceImplementationName("ToString") && member is IMethodSymbol { Arity: 0, IsStatic: false, Parameters.Length: 2, } method &&
+ method.Parameters[0].Type.SpecialType == SpecialType.System_String && method.Parameters[1].Type.IsSystemType("IFormatProvider")));
+
+ existingComponents |= WrapperValueObjectTypeComponents.ParsableTryParseMethod.If(members.Any(member =>
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 3, } method &&
+ member.HasNameOrExplicitInterfaceImplementationName("TryParse") &&
+ method.Parameters[0].Type.SpecialType == SpecialType.System_String && method.Parameters[1].Type.IsSystemType("IFormatProvider") && method.Parameters[2].Type.Equals(type, SymbolEqualityComparer.Default) && method.Parameters[2].RefKind == RefKind.Out));
+
+ existingComponents |= WrapperValueObjectTypeComponents.ParsableParseMethod.If(members.Any(member =>
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 2, } method &&
+ member.HasNameOrExplicitInterfaceImplementationName("Parse") &&
+ method.Parameters[0].Type.SpecialType == SpecialType.System_String && method.Parameters[1].Type.IsSystemType("IFormatProvider")));
+
+ existingComponents |= WrapperValueObjectTypeComponents.SpanFormattableTryFormatMethod.If(members.Any(member =>
+ member is IMethodSymbol { Arity: 0, IsStatic: false, Parameters.Length: 4, } method &&
+ member.HasNameOrExplicitInterfaceImplementationName("TryFormat") &&
+ method.Parameters[0].Type.IsSpanOfSpecialType(SpecialType.System_Char) &&
+ method.Parameters[1].Type.SpecialType == SpecialType.System_Int32 && method.Parameters[1].RefKind == RefKind.Out &&
+ method.Parameters[2].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Char) &&
+ method.Parameters[3].Type.IsSystemType("IFormatProvider")));
+
+ existingComponents |= WrapperValueObjectTypeComponents.SpanParsableTryParseMethod.If(members.Any(member =>
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 3, } method &&
+ member.HasNameOrExplicitInterfaceImplementationName("TryParse") &&
+ method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Char) &&
+ method.Parameters[1].Type.IsSystemType("IFormatProvider") &&
+ method.Parameters[2].Type.Equals(type, SymbolEqualityComparer.Default) && method.Parameters[2].RefKind == RefKind.Out));
+
+ existingComponents |= WrapperValueObjectTypeComponents.SpanParsableParseMethod.If(members.Any(member =>
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 2, } method &&
+ member.HasNameOrExplicitInterfaceImplementationName("Parse") &&
+ method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Char) &&
+ method.Parameters[1].Type.IsSystemType("IFormatProvider")));
+
+ existingComponents |= WrapperValueObjectTypeComponents.Utf8SpanFormattableTryFormatMethod.If(members.Any(member =>
+ member is IMethodSymbol { Arity: 0, IsStatic: false, Parameters.Length: 4, } method &&
+ member.HasNameOrExplicitInterfaceImplementationName("TryFormat") &&
+ method.Parameters[0].Type.IsSpanOfSpecialType(SpecialType.System_Byte) &&
+ method.Parameters[1].Type.SpecialType == SpecialType.System_Int32 && method.Parameters[1].RefKind == RefKind.Out &&
+ method.Parameters[2].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Char) &&
+ method.Parameters[3].Type.IsSystemType("IFormatProvider")));
+
+ existingComponents |= WrapperValueObjectTypeComponents.Utf8SpanParsableTryParseMethod.If(members.Any(member =>
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 3, } method &&
+ member.HasNameOrExplicitInterfaceImplementationName("TryParse") &&
+ method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Byte) &&
+ method.Parameters[1].Type.IsSystemType("IFormatProvider") &&
+ method.Parameters[2].Type.Equals(type, SymbolEqualityComparer.Default) && method.Parameters[2].RefKind == RefKind.Out));
+
+ existingComponents |= WrapperValueObjectTypeComponents.Utf8SpanParsableParseMethod.If(members.Any(member =>
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 2, } method &&
+ member.HasNameOrExplicitInterfaceImplementationName("Parse") &&
+ method.Parameters[0].Type.IsReadOnlySpanOfSpecialType(SpecialType.System_Byte) &&
+ method.Parameters[1].Type.IsSystemType("IFormatProvider")));
existingComponents |= WrapperValueObjectTypeComponents.CreateMethod.If(members.Any(member =>
- member is IMethodSymbol method && method.IsStatic && method.Arity == 0 && method.Parameters.Length == 1 &&
+ member is IMethodSymbol { Arity: 0, IsStatic: true, Parameters.Length: 1, } method &&
member.HasNameOrExplicitInterfaceImplementationName("Create") &&
- method.Parameters[0].Type.Equals(underlyingType, SymbolEqualityComparer.Default)));
+ method.Parameters[0].Type.Equals(underlyingType, SymbolEqualityComparer.Default) &&
+ method.ReturnType.Equals(type, SymbolEqualityComparer.Default)));
+
+ existingComponents |= WrapperValueObjectTypeComponents.DirectValueWrapperInterface.If(type.AllInterfaces.Any(interf =>
+ interf.IsType("IDirectValueWrapper", "Architect", "DomainModeling", arity: 2) && !interf.IsImplicitlyDeclared &&
+ interf.TypeArguments[0].Equals(type, SymbolEqualityComparer.Default) && interf.TypeArguments[1].Equals(underlyingType, SymbolEqualityComparer.Default)));
+
+ existingComponents |= WrapperValueObjectTypeComponents.CoreValueWrapperInterface.If(type.AllInterfaces.Any(interf =>
+ interf.IsType("ICoreValueWrapper", "Architect", "DomainModeling", arity: 2) && !interf.IsImplicitlyDeclared &&
+ interf.TypeArguments[0].Equals(type, SymbolEqualityComparer.Default)));
result.ExistingComponents = existingComponents;
result.ValueMemberLocation = members.FirstOrDefault(member => member.Name == "Value" && member is IFieldSymbol or IPropertySymbol)?.Locations.FirstOrDefault();
@@ -331,6 +386,8 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
result.IsToStringNullable = !members.Any(member =>
member.Name == "Value" && member is IPropertySymbol { GetMethod.ReturnType: { SpecialType: SpecialType.System_String, NullableAnnotation: NullableAnnotation.NotAnnotated, } });
+ result.Problem = GetFirstProblem(tds, type, underlyingType);
+
return result;
}
@@ -341,69 +398,17 @@ private static void GenerateSource(SourceProductionContext context, (Generatable
var generatable = input.Generatable;
var valueWrappers = input.ValueWrappers;
- (var isSpanFormattable, var isSpanParsable, var isUtf8SpanFormattable, var isUtf8SpanParsable) = ValueWrapperGenerator.GetFormattabilityAndParsabilityRecursively(
- valueWrappers,
- typeName: generatable.TypeName, containingNamespace: generatable.ContainingNamespace, underlyingTypeFullyQualifiedName: generatable.UnderlyingTypeFullyQualifiedName);
-
- // Require the expected inheritance
- if (!generatable.IsPartial && !generatable.IsWrapperValueObject)
- {
- context.ReportDiagnostic("WrapperValueObjectGeneratorUnexpectedInheritance", "Unexpected inheritance",
- "Type marked as wrapper value object lacks IWrapperValueObject interface. Did you forget the 'partial' keyword and elude source generation?", DiagnosticSeverity.Warning, generatable.TypeLocation);
- return;
- }
+ if (generatable.Problem is not null)
+ context.ReportDiagnostic(generatable.Problem);
- // Require ISerializableDomainObject
- if (!generatable.IsPartial && !generatable.IsSerializableDomainObject)
- {
- context.ReportDiagnostic("WrapperValueObjectGeneratorMissingSerializableDomainObject", "Missing interface",
- "Type marked as wrapper value object lacks ISerializableDomainObject interface.", DiagnosticSeverity.Warning, generatable.TypeLocation);
+ if (generatable.Problem is not null || !generatable.IsPartial)
return;
- }
- // No source generation, only above analyzers
- if (!generatable.IsPartial)
- return;
-
- // Only if class
- if (!generatable.IsClass)
- {
- context.ReportDiagnostic("WrapperValueObjectGeneratorValueType", "Source-generated struct wrapper value object",
- "The type was not source-generated because it is a struct, while a class was expected. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning, generatable.TypeLocation);
- return;
- }
-
- // Only if non-record
- if (generatable.IsRecord)
- {
- context.ReportDiagnostic("WrapperValueObjectGeneratorRecordType", "Source-generated record wrapper value object",
- "The type was not source-generated because it is a record, which cannot inherit from a non-record base class. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning, generatable.TypeLocation);
- return;
- }
+ var coreTypeFullyQualifiedName = ValueWrapperGenerator.GetCoreTypeFullyQualifiedName(valueWrappers, generatable.TypeName, generatable.ContainingNamespace);
- // Only if non-abstract
- if (generatable.IsAbstract)
- {
- context.ReportDiagnostic("WrapperValueObjectGeneratorAbstractType", "Source-generated abstract type",
- "The type was not source-generated because it is abstract. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning, generatable.TypeLocation);
- return;
- }
-
- // Only if non-generic
- if (generatable.IsGeneric)
- {
- context.ReportDiagnostic("WrapperValueObjectGeneratorGenericType", "Source-generated generic type",
- "The type was not source-generated because it is generic. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning, generatable.TypeLocation);
- return;
- }
-
- // Only if non-nested
- if (generatable.IsNested)
- {
- context.ReportDiagnostic("WrapperValueObjectGeneratorNestedType", "Source-generated nested type",
- "The type was not source-generated because it is a nested type. To get source generation, avoid nesting it inside another type. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning, generatable.TypeLocation);
- return;
- }
+ (var isSpanFormattable, var isSpanParsable, var isUtf8SpanFormattable, var isUtf8SpanParsable) = ValueWrapperGenerator.GetFormattabilityAndParsabilityRecursively(
+ valueWrappers,
+ typeName: generatable.TypeName, containingNamespace: generatable.ContainingNamespace);
var typeName = generatable.TypeName;
var containingNamespace = generatable.ContainingNamespace;
@@ -417,7 +422,7 @@ private static void GenerateSource(SourceProductionContext context, (Generatable
: $"Wrapper<{typeName}, {underlyingTypeFullyQualifiedName}>";
// Warn if Value is not settable
- if (existingComponents.HasFlag(WrapperValueObjectTypeComponents.UnsettableValue))
+ if (existingComponents.HasFlags(WrapperValueObjectTypeComponents.UnsettableValue))
context.ReportDiagnostic("WrapperValueObjectGeneratorUnsettableValue", "WrapperValueObject has Value property without init",
"The WrapperValueObject's Value property is missing 'private init' and is using a workaround to be deserializable. To support deserialization more cleanly, use '{ get; private init; }' or let the source generator implement the property.",
DiagnosticSeverity.Warning, generatable.ValueMemberLocation);
@@ -426,6 +431,7 @@ private static void GenerateSource(SourceProductionContext context, (Generatable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
using Architect.DomainModeling;
using Architect.DomainModeling.Conversions;
@@ -433,24 +439,18 @@ private static void GenerateSource(SourceProductionContext context, (Generatable
namespace {containingNamespace}
{{
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.SystemTextJsonConverter) ? "/*" : "")}
- {JsonSerializationGenerator.WriteJsonConverterAttribute(typeName, underlyingTypeFullyQualifiedName)}
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.SystemTextJsonConverter) ? "*/" : "")}
-
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.NewtonsoftJsonConverter) ? "/*" : "")}
- {JsonSerializationGenerator.WriteNewtonsoftJsonConverterAttribute(typeName, underlyingTypeFullyQualifiedName)}
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.NewtonsoftJsonConverter) ? "*/" : "")}
-
+ {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.SystemTextJsonConverter) ? "//" : "")}{JsonSerializationGenerator.WriteJsonConverterAttribute(typeName, underlyingTypeFullyQualifiedName)}
+ {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.NewtonsoftJsonConverter) ? "//" : "")}{JsonSerializationGenerator.WriteNewtonsoftJsonConverterAttribute(typeName, underlyingTypeFullyQualifiedName)}
/* Generated */ {generatable.Accessibility.ToCodeString()} sealed partial{(generatable.IsRecord ? " record" : "")} class {typeName} :
WrapperValueObject<{underlyingTypeFullyQualifiedName}>,
- IValueWrapper<{typeName}, {underlyingTypeFullyQualifiedName}>,
IEquatable<{typeName}>,
{(isComparable ? "" : "//")}IComparable<{typeName}>,
{(isSpanFormattable ? "" : "//")}ISpanFormattable, ISpanFormattable{formattableParsableWrapperSuffix},
{(isSpanParsable ? "" : "//")}ISpanParsable<{typeName}>, ISpanParsable{formattableParsableWrapperSuffix},
{(isUtf8SpanFormattable ? "" : "//")}IUtf8SpanFormattable, IUtf8SpanFormattable{formattableParsableWrapperSuffix},
{(isUtf8SpanParsable ? "" : "//")}IUtf8SpanParsable<{typeName}>, IUtf8SpanParsable{formattableParsableWrapperSuffix},
- ISerializableDomainObject<{typeName}, {underlyingTypeFullyQualifiedName}>
+ IDirectValueWrapper<{typeName}, {underlyingTypeFullyQualifiedName}>,
+ ICoreValueWrapper<{typeName}, {coreTypeFullyQualifiedName}>
{{
{(existingComponents.HasFlags(WrapperValueObjectTypeComponents.StringComparison) ? "/*" : "")}
{(generatable.UnderlyingTypeIsString ? "" : @"protected sealed override StringComparison StringComparison => throw new NotSupportedException(""This operation applies to string-based value objects only."");")}
@@ -477,6 +477,7 @@ namespace {containingNamespace}
{(existingComponents.HasFlags(WrapperValueObjectTypeComponents.DefaultConstructor) ? "*/" : "")}
{(existingComponents.HasFlags(WrapperValueObjectTypeComponents.CreateMethod) ? "/*" : "")}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
static {typeName} IValueWrapper<{typeName}, {underlyingTypeFullyQualifiedName}>.Create({underlyingTypeFullyQualifiedName} value)
{{
return new {typeName}(value);
@@ -487,7 +488,9 @@ namespace {containingNamespace}
///
/// Serializes a domain object as a plain value.
///
- {underlyingTypeFullyQualifiedName}{(generatable.UnderlyingTypeIsStruct ? "" : "?")} ISerializableDomainObject<{typeName}, {underlyingTypeFullyQualifiedName}>.Serialize()
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [return: MaybeNull]
+ {underlyingTypeFullyQualifiedName} IValueWrapper<{typeName}, {underlyingTypeFullyQualifiedName}>.Serialize()
{{
return this.Value;
}}
@@ -501,7 +504,8 @@ namespace {containingNamespace}
///
/// Deserializes a plain value back into a domain object, without using a parameterized constructor.
///
- static {typeName} ISerializableDomainObject<{typeName}, {underlyingTypeFullyQualifiedName}>.Deserialize({underlyingTypeFullyQualifiedName} value)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static {typeName} IValueWrapper<{typeName}, {underlyingTypeFullyQualifiedName}>.Deserialize({underlyingTypeFullyQualifiedName} value)
{{
{(existingComponents.HasFlags(WrapperValueObjectTypeComponents.UnsettableValue) ? $@"
// To instead get syntax that is safe at compile time, make the Value property '{{ get; private init; }}' (or let the source generator implement it)
@@ -512,6 +516,39 @@ namespace {containingNamespace}
}}
{(existingComponents.HasFlags(WrapperValueObjectTypeComponents.DeserializeFromUnderlying) ? "*/" : "")}
+ {(generatable.ExistingComponents.HasFlags(WrapperValueObjectTypeComponents.CoreValueWrapperInterface) ? "/* Core manually specified" : coreTypeFullyQualifiedName == underlyingTypeFullyQualifiedName ? "/* For nested wrapper types only" : "")}
+ [MaybeNull]
+ {coreTypeFullyQualifiedName} IValueWrapper<{typeName}, {coreTypeFullyQualifiedName}>.Value => ValueWrapperUnwrapper.Unwrap<{underlyingTypeFullyQualifiedName}, {coreTypeFullyQualifiedName}>(this.Value);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static {typeName} IValueWrapper<{typeName}, {coreTypeFullyQualifiedName}>.Create({coreTypeFullyQualifiedName} value)
+ {{
+ var intermediateValue = ValueWrapperUnwrapper.Wrap<{underlyingTypeFullyQualifiedName}, {coreTypeFullyQualifiedName}>(value);
+ return ValueWrapperUnwrapper.Wrap<{typeName}, {underlyingTypeFullyQualifiedName}>(intermediateValue);
+ }}
+
+ ///
+ /// Serializes a domain object as a plain value.
+ ///
+ [return: MaybeNull]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ {coreTypeFullyQualifiedName} IValueWrapper<{typeName}, {coreTypeFullyQualifiedName}>.Serialize()
+ {{
+ return DomainObjectSerializer.Serialize<{underlyingTypeFullyQualifiedName}, {coreTypeFullyQualifiedName}>(
+ DomainObjectSerializer.Serialize<{typeName}, {underlyingTypeFullyQualifiedName}>(this));
+ }}
+
+ ///
+ /// Deserializes a plain value back into a domain object, without using a parameterized constructor.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static {typeName} IValueWrapper<{typeName}, {coreTypeFullyQualifiedName}>.Deserialize({coreTypeFullyQualifiedName} value)
+ {{
+ var intermediateValue = DomainObjectSerializer.Deserialize<{underlyingTypeFullyQualifiedName}, {coreTypeFullyQualifiedName}>(value);
+ return DomainObjectSerializer.Deserialize<{typeName}, {underlyingTypeFullyQualifiedName}>(intermediateValue);
+ }}
+ {(coreTypeFullyQualifiedName == underlyingTypeFullyQualifiedName ? "*/" : generatable.ExistingComponents.HasFlags(WrapperValueObjectTypeComponents.CoreValueWrapperInterface) ? "*/" : "")}
+
{(existingComponents.HasFlags(WrapperValueObjectTypeComponents.ToStringOverride) ? "/*" : "")}
public sealed override string{(generatable.IsToStringNullable ? "?" : "")} ToString()
{{
@@ -555,47 +592,26 @@ public int CompareTo({typeName}? other)
}}
{(existingComponents.HasFlags(WrapperValueObjectTypeComponents.CompareToMethod) || !isComparable ? "*/" : "")}
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.EqualsOperator) ? "/*" : "")}
- public static bool operator ==({typeName}? left, {typeName}? right) => left is null ? right is null : left.Equals(right);
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.EqualsOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.NotEqualsOperator) ? "/*" : "")}
- public static bool operator !=({typeName}? left, {typeName}? right) => !(left == right);
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.NotEqualsOperator) ? "*/" : "")}
+ {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.EqualsOperator) ? "//" : "")}public static bool operator ==({typeName}? left, {typeName}? right) => left is null ? right is null : left.Equals(right);
+ {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.NotEqualsOperator) ? "//" : "")}public static bool operator !=({typeName}? left, {typeName}? right) => !(left == right);
{(isComparable ? "" : "/*")}
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.GreaterThanOperator) ? "/*" : "")}
- public static bool operator >({typeName}? left, {typeName}? right) => left is null ? false : left.CompareTo(right) > 0;
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.GreaterThanOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.LessThanOperator) ? "/*" : "")}
- public static bool operator <({typeName}? left, {typeName}? right) => left is null ? right is not null : left.CompareTo(right) < 0;
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.LessThanOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.GreaterEqualsOperator) ? "/*" : "")}
- public static bool operator >=({typeName}? left, {typeName}? right) => !(left < right);
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.GreaterEqualsOperator) ? "*/" : "")}
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.LessEqualsOperator) ? "/*" : "")}
- public static bool operator <=({typeName}? left, {typeName}? right) => !(left > right);
- {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.LessEqualsOperator) ? "*/" : "")}
+ {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.GreaterThanOperator) ? "//" : "")}public static bool operator >({typeName}? left, {typeName}? right) => left is null ? false : left.CompareTo(right) > 0;
+ {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.LessThanOperator) ? "//" : "")}public static bool operator <({typeName}? left, {typeName}? right) => left is null ? right is not null : left.CompareTo(right) < 0;
+ {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.GreaterEqualsOperator) ? "//" : "")}public static bool operator >=({typeName}? left, {typeName}? right) => !(left < right);
+ {(existingComponents.HasFlags(WrapperValueObjectTypeComponents.LessEqualsOperator) ? "//" : "")}public static bool operator <=({typeName}? left, {typeName}? right) => !(left > right);
{(isComparable ? "" : "*/")}
- {(generatable.UnderlyingTypeKind == TypeKind.Interface || existingComponents.HasFlags(WrapperValueObjectTypeComponents.ConvertToOperator) ? "/*" : "")}
- {(generatable.UnderlyingTypeIsStruct ? "" : @"[return: NotNullIfNotNull(""value"")]")}
- public static explicit operator {typeName}{(generatable.UnderlyingTypeIsStruct ? "" : "?")}({underlyingTypeFullyQualifiedName}{(generatable.UnderlyingTypeIsStruct ? "" : "?")} value) => {(generatable.UnderlyingTypeIsStruct ? "" : "value is null ? null : ")}new {typeName}(value);
- {(generatable.UnderlyingTypeKind == TypeKind.Interface || existingComponents.HasFlags(WrapperValueObjectTypeComponents.ConvertToOperator) ? "*/" : "")}
-
- {(generatable.UnderlyingTypeKind == TypeKind.Interface || existingComponents.HasFlags(WrapperValueObjectTypeComponents.ConvertFromOperator) ? "/*" : "")}
- {(generatable.UnderlyingTypeIsStruct ? "" : @"[return: NotNullIfNotNull(""instance"")]")}
- public static implicit operator {underlyingTypeFullyQualifiedName}{(generatable.UnderlyingTypeIsStruct ? "" : "?")}({typeName}{(generatable.UnderlyingTypeIsStruct ? "" : "?")} instance) => instance{(generatable.UnderlyingTypeIsStruct ? "" : "?")}.Value;
- {(generatable.UnderlyingTypeKind == TypeKind.Interface || existingComponents.HasFlags(WrapperValueObjectTypeComponents.ConvertFromOperator) ? "*/" : "")}
-
- {(generatable.UnderlyingTypeIsNullable || existingComponents.HasFlags(WrapperValueObjectTypeComponents.NullableConvertToOperator) ? "/*" : "")}
- {(generatable.UnderlyingTypeIsStruct ? @"[return: NotNullIfNotNull(""value"")]" : "")}
- {(generatable.UnderlyingTypeIsStruct ? $"public static explicit operator {typeName}?({underlyingTypeFullyQualifiedName}? value) => value is null ? null : new {typeName}(value.Value);" : "")}
- {(generatable.UnderlyingTypeIsNullable || existingComponents.HasFlags(WrapperValueObjectTypeComponents.NullableConvertToOperator) ? "*/" : "")}
+ {(generatable.UnderlyingTypeKind == TypeKind.Interface || existingComponents.HasFlags(WrapperValueObjectTypeComponents.ConvertToOperator) ? "//" : "")}
+ {(generatable.UnderlyingTypeKind == TypeKind.Interface || existingComponents.HasFlags(WrapperValueObjectTypeComponents.ConvertToOperator) ? "//" : "")}{(generatable.UnderlyingTypeIsStruct ? "[return: NotNull]" : @"[return: NotNullIfNotNull(""value"")]")}
+ {(generatable.UnderlyingTypeKind == TypeKind.Interface || existingComponents.HasFlags(WrapperValueObjectTypeComponents.ConvertToOperator) ? "//" : "")}public static explicit operator {typeName}{(generatable.UnderlyingTypeIsStruct ? "" : "?")}({underlyingTypeFullyQualifiedName}{(generatable.UnderlyingTypeIsStruct ? "" : "?")} value) => {(generatable.UnderlyingTypeIsStruct ? "" : "value is null ? null : ")}new {typeName}(value);
+ {(generatable.UnderlyingTypeKind == TypeKind.Interface || existingComponents.HasFlags(WrapperValueObjectTypeComponents.ConvertFromOperator) ? "//" : "")}{(generatable.UnderlyingTypeIsStruct ? "[return: NotNull]" : @"[return: NotNullIfNotNull(""instance"")]")}
+ {(generatable.UnderlyingTypeKind == TypeKind.Interface || existingComponents.HasFlags(WrapperValueObjectTypeComponents.ConvertFromOperator) ? "//" : "")}public static implicit operator {underlyingTypeFullyQualifiedName}{(generatable.UnderlyingTypeIsStruct ? "" : "?")}({typeName}{(generatable.UnderlyingTypeIsStruct ? "" : "?")} instance) => instance{(generatable.UnderlyingTypeIsStruct ? "" : "?")}.Value;
- {(generatable.UnderlyingTypeIsNullable || existingComponents.HasFlags(WrapperValueObjectTypeComponents.NullableConvertFromOperator) ? "/*" : "")}
- {(generatable.UnderlyingTypeIsStruct ? @"[return: NotNullIfNotNull(""instance"")]" : "")}
- {(generatable.UnderlyingTypeIsStruct ? $"public static implicit operator {underlyingTypeFullyQualifiedName}?({typeName}? instance) => instance?.Value;" : "")}
- {(generatable.UnderlyingTypeIsNullable || existingComponents.HasFlags(WrapperValueObjectTypeComponents.NullableConvertFromOperator) ? "*/" : "")}
+ {(generatable.UnderlyingTypeIsNullable || existingComponents.HasFlags(WrapperValueObjectTypeComponents.NullableConvertToOperator) ? "//" : "")}{(generatable.UnderlyingTypeIsStruct ? @"[return: NotNullIfNotNull(""value"")]" : "")}
+ {(generatable.UnderlyingTypeIsNullable || existingComponents.HasFlags(WrapperValueObjectTypeComponents.NullableConvertToOperator) ? "//" : "")}{(generatable.UnderlyingTypeIsStruct ? $"public static explicit operator {typeName}?({underlyingTypeFullyQualifiedName}? value) => value is null ? null : new {typeName}(value.Value);" : "")}
+ {(generatable.UnderlyingTypeIsNullable || existingComponents.HasFlags(WrapperValueObjectTypeComponents.NullableConvertFromOperator) ? "//" : "")}{(generatable.UnderlyingTypeIsStruct ? @"[return: NotNullIfNotNull(""instance"")]" : "")}
+ {(generatable.UnderlyingTypeIsNullable || existingComponents.HasFlags(WrapperValueObjectTypeComponents.NullableConvertFromOperator) ? "//" : "")}{(generatable.UnderlyingTypeIsStruct ? $"public static implicit operator {underlyingTypeFullyQualifiedName}?({typeName}? instance) => instance?.Value;" : "")}
#region Formatting & Parsing
@@ -701,29 +717,30 @@ internal enum WrapperValueObjectTypeComponents : ulong
Utf8SpanParsableTryParseMethod = 1UL << 31,
Utf8SpanParsableParseMethod = 1UL << 32,
CreateMethod = 1UL << 33,
+ DirectValueWrapperInterface = 1UL << 34,
+ CoreValueWrapperInterface = 1UL << 35,
}
private sealed record Generatable
{
private uint _bits;
public bool IsWrapperValueObject { get => this._bits.GetBit(0); set => this._bits.SetBit(0, value); }
- public bool IsSerializableDomainObject { get => this._bits.GetBit(1); set => this._bits.SetBit(1, value); }
- public bool IsPartial { get => this._bits.GetBit(2); set => this._bits.SetBit(2, value); }
- public bool IsRecord { get => this._bits.GetBit(3); set => this._bits.SetBit(3, value); }
- public bool IsClass { get => this._bits.GetBit(4); set => this._bits.SetBit(4, value); }
- public bool IsAbstract { get => this._bits.GetBit(5); set => this._bits.SetBit(5, value); }
- public bool IsGeneric { get => this._bits.GetBit(6); set => this._bits.SetBit(6, value); }
- public bool IsNested { get => this._bits.GetBit(7); set => this._bits.SetBit(7, value); }
- public bool IsComparable { get => this._bits.GetBit(8); set => this._bits.SetBit(8, value); }
+ public bool IsPartial { get => this._bits.GetBit(1); set => this._bits.SetBit(1, value); }
+ public bool IsRecord { get => this._bits.GetBit(2); set => this._bits.SetBit(2, value); }
+ public bool IsClass { get => this._bits.GetBit(3); set => this._bits.SetBit(3, value); }
+ public bool IsAbstract { get => this._bits.GetBit(4); set => this._bits.SetBit(4, value); }
+ public bool IsGeneric { get => this._bits.GetBit(5); set => this._bits.SetBit(5, value); }
+ public bool IsNested { get => this._bits.GetBit(6); set => this._bits.SetBit(6, value); }
+ public bool IsComparable { get => this._bits.GetBit(7); set => this._bits.SetBit(7, value); }
public string TypeName { get; set; } = null!;
public string ContainingNamespace { get; set; } = null!;
public string UnderlyingTypeFullyQualifiedName { get; set; } = null!;
public TypeKind UnderlyingTypeKind { get; set; }
- public bool UnderlyingTypeIsStruct { get => this._bits.GetBit(9); set => this._bits.SetBit(9, value); }
- public bool UnderlyingTypeIsNullable { get => this._bits.GetBit(10); set => this._bits.SetBit(10, value); }
- public bool UnderlyingTypeIsString { get => this._bits.GetBit(11); set => this._bits.SetBit(11, value); }
- public bool IsToStringNullable { get => this._bits.GetBit(12); set => this._bits.SetBit(12, value); }
- public bool UnderlyingTypeIsInterface { get => this._bits.GetBit(13); set => this._bits.SetBit(13, value); }
+ public bool UnderlyingTypeIsStruct { get => this._bits.GetBit(8); set => this._bits.SetBit(8, value); }
+ public bool UnderlyingTypeIsNullable { get => this._bits.GetBit(9); set => this._bits.SetBit(9, value); }
+ public bool UnderlyingTypeIsString { get => this._bits.GetBit(10); set => this._bits.SetBit(10, value); }
+ public bool IsToStringNullable { get => this._bits.GetBit(11); set => this._bits.SetBit(11, value); }
+ public bool UnderlyingTypeIsInterface { get => this._bits.GetBit(12); set => this._bits.SetBit(12, value); }
public string ValueFieldName { get; set; } = null!;
public Accessibility Accessibility { get; set; }
public WrapperValueObjectTypeComponents ExistingComponents { get; set; }
@@ -731,7 +748,7 @@ private sealed record Generatable
public string HashCodeExpression { get; set; } = null!;
public string EqualityExpression { get; set; } = null!;
public string ComparisonExpression { get; set; } = null!;
- public SimpleLocation? TypeLocation { get; set; }
public SimpleLocation? ValueMemberLocation { get; set; }
+ public Diagnostic? Problem { get; set; }
}
}
diff --git a/DomainModeling.Tests/EntityFramework/EntityFrameworkConfigurationGeneratorTests.cs b/DomainModeling.Tests/EntityFramework/EntityFrameworkConfigurationGeneratorTests.cs
index 4f0c9f6..d73677d 100644
--- a/DomainModeling.Tests/EntityFramework/EntityFrameworkConfigurationGeneratorTests.cs
+++ b/DomainModeling.Tests/EntityFramework/EntityFrameworkConfigurationGeneratorTests.cs
@@ -1,5 +1,8 @@
+using Architect.DomainModeling.Conversions;
+using Architect.DomainModeling.Tests.IdentityTestTypes;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Xunit;
namespace Architect.DomainModeling.Tests.EntityFramework;
@@ -25,7 +28,13 @@ public void Dispose()
[Fact]
public void ConfigureConventions_WithAllExtensionsCalled_ShouldBeAbleToWorkWithAllDomainObjects()
{
- var values = new ValueObjectForEF((Wrapper1ForEF)"One", (Wrapper2ForEF)2);
+ var values = new ValueObjectForEF(
+ (Wrapper1ForEF)"One",
+ (Wrapper2ForEF)2,
+ new FormatAndParseTestingIntId(3),
+ new LazyStringWrapper(new Lazy("4")),
+ new LazyIntWrapper(new Lazy(5)),
+ new NumericStringId("6"));
var entity = new EntityForEF(values);
var domainEvent = new DomainEventForEF(id: 2, ignored: null!);
@@ -55,6 +64,18 @@ public void ConfigureConventions_WithAllExtensionsCalled_ShouldBeAbleToWorkWithA
Assert.Equal(2, reloadedEntity.Id.Value);
Assert.Equal("One", reloadedEntity.Values.One);
Assert.Equal(2m, reloadedEntity.Values.Two);
+ Assert.Equal(3, reloadedEntity.Values.Three.Value?.Value.Value);
+ Assert.Equal("4", reloadedEntity.Values.Four.Value.Value);
+ Assert.Equal(5, reloadedEntity.Values.Five.Value.Value);
+ Assert.Equal("6", reloadedEntity.Values.Six.Value);
+
+ // This property should be mapped to int via ICoreValueWrapper
+ var mappingForStringWithCustomIntCore = this.DbContext.Model.FindEntityType(typeof(EntityForEF))?.FindNavigation(nameof(EntityForEF.Values))?.TargetEntityType
+ .FindProperty(nameof(EntityForEF.Values.Six));
+ var columnTypeForStringWrapperWithCustomIntCore = mappingForStringWithCustomIntCore?.GetColumnType();
+ var providerClrTypeForStringWrapperWithCustomIntCore = mappingForStringWithCustomIntCore?.GetValueConverter()?.ProviderClrType;
+ Assert.Equal("INTEGER", columnTypeForStringWrapperWithCustomIntCore);
+ Assert.Equal(typeof(int), providerClrTypeForStringWrapperWithCustomIntCore);
}
}
@@ -75,6 +96,22 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura
domainModel.ConfigureEntityConventions();
domainModel.ConfigureDomainEventConventions();
});
+
+ // For a wrapper whose core type EF does not support, overwriting the conventions with our own should work
+ configurationBuilder.Properties()
+ .HaveConversion();
+ configurationBuilder.DefaultTypeMapping()
+ .HasConversion();
+ }
+
+ private class LazyStringWrapperConverter : ValueConverter
+ {
+ public LazyStringWrapperConverter()
+ : base(
+ v => v.Value.Value,
+ v => new LazyStringWrapper(new Lazy(v)))
+ {
+ }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
@@ -90,6 +127,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
{
values.Property(x => x.One);
values.Property(x => x.Two);
+ values.Property(x => x.Three);
+ values.Property(x => x.Four);
+ values.Property(x => x.Five);
+ values.Property(x => x.Six);
});
builder.HasKey(x => x.Id);
@@ -192,6 +233,31 @@ public Wrapper2ForEF(decimal value)
}
}
+[WrapperValueObject>]
+internal sealed partial class LazyStringWrapper
+{
+}
+
+[WrapperValueObject>]
+internal sealed partial class LazyIntWrapper : ICoreValueWrapper // Custom core value
+{
+ // Manual interface implementation to support custom core value
+ int IValueWrapper.Value => this.Value.Value;
+ static LazyIntWrapper IValueWrapper.Create(int value) => new LazyIntWrapper(new Lazy(value));
+ int IValueWrapper.Serialize() => this.Value.Value;
+ static LazyIntWrapper IValueWrapper.Deserialize(int value) => DomainObjectSerializer.Deserialize>(new Lazy(value));
+}
+
+[IdentityValueObject]
+internal partial struct NumericStringId : ICoreValueWrapper // Custom core value
+{
+ // Manual interface implementation to support custom core value
+ int IValueWrapper.Value => Int32.Parse(this.Value);
+ static NumericStringId IValueWrapper.Create(int value) => new NumericStringId(value.ToString());
+ int IValueWrapper.Serialize() => Int32.Parse(this.Value);
+ static NumericStringId IValueWrapper.Deserialize(int value) => DomainObjectSerializer.Deserialize(value.ToString());
+}
+
[ValueObject]
internal sealed partial class ValueObjectForEF
{
@@ -202,13 +268,21 @@ internal sealed partial class ValueObjectForEF
public Wrapper1ForEF One { get; private init; }
public Wrapper2ForEF Two { get; private init; }
+ public FormatAndParseTestingIntId Three { get; private init; }
+ public LazyStringWrapper Four { get; private init; }
+ public LazyIntWrapper Five { get; private init; }
+ public NumericStringId Six { get; private init; }
- public ValueObjectForEF(Wrapper1ForEF one, Wrapper2ForEF two)
+ public ValueObjectForEF(Wrapper1ForEF one, Wrapper2ForEF two, FormatAndParseTestingIntId three, LazyStringWrapper four, LazyIntWrapper five, NumericStringId six)
{
if (!EntityFrameworkConfigurationGeneratorTests.AllowParameterizedConstructors)
throw new InvalidOperationException("Deserialization was not allowed to use the parameterized constructors.");
this.One = one;
this.Two = two;
+ this.Three = three;
+ this.Four = four;
+ this.Five = five;
+ this.Six = six;
}
}
diff --git a/DomainModeling.Tests/IdentityTests.cs b/DomainModeling.Tests/IdentityTests.cs
index c7d5185..b8533b8 100644
--- a/DomainModeling.Tests/IdentityTests.cs
+++ b/DomainModeling.Tests/IdentityTests.cs
@@ -352,6 +352,98 @@ public void CastFromNullableUnderlyingType_Regularly_ShouldReturnExpectedResult(
Assert.Equal(expectedResult, result?.Value);
}
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Value_ViaCoreValueInterface_ShouldReturnExpectedResult(int value)
+ {
+ ICoreValueWrapper intInstance =
+ new FormatAndParseTestingIntId(value);
+ Assert.IsType(intInstance.Value);
+ Assert.Equal(value, intInstance.Value);
+
+ ICoreValueWrapper stringInstance =
+ new FormatAndParseTestingStringId(new StringValue(value.ToString()));
+ Assert.IsType(stringInstance.Value);
+ Assert.Equal(value.ToString(), stringInstance.Value);
+ }
+
+ ///
+ /// Helper to access abstract statics.
+ ///
+ private static TWrapper CreateFromDirectUnderlyingValue(TValue value)
+ where TWrapper : IDirectValueWrapper
+ {
+ return TWrapper.Create(value);
+ }
+
+ ///
+ /// Helper to access abstract statics.
+ ///
+ private static TWrapper CreateFromCoreValue(TValue value)
+ where TWrapper : ICoreValueWrapper
+ {
+ return TWrapper.Create(value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Create_ViaDirectUnderlyingValueInterface_ShouldReturnExpectedResult(int value)
+ {
+ var intInstance = new FormatAndParseTestingIntWrapper(value);
+ Assert.IsType(CreateFromDirectUnderlyingValue(intInstance));
+ Assert.Equal(value, CreateFromDirectUnderlyingValue(intInstance).Value?.Value.Value);
+
+ var stringInstance = new StringValue(value.ToString());
+ Assert.IsType(CreateFromDirectUnderlyingValue(stringInstance));
+ Assert.Equal(value.ToString(), CreateFromDirectUnderlyingValue(stringInstance).Value?.Value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Create_ViaCoreValueInterface_ShouldReturnExpectedResult(int value)
+ {
+ Assert.IsType(CreateFromCoreValue(value));
+ Assert.Equal(value, CreateFromCoreValue(value).Value?.Value.Value);
+
+ Assert.IsType(CreateFromCoreValue(value.ToString()));
+ Assert.Equal(value.ToString(), CreateFromCoreValue(value.ToString()).Value?.Value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Serialize_ToImmediateUnderlyingType_ShouldReturnExpectedResult(int value)
+ {
+ IValueWrapper intInstance =
+ new FormatAndParseTestingIntId(value);
+ Assert.IsType(intInstance.Serialize());
+ Assert.Equal(value, intInstance.Serialize()?.Value.Value);
+
+ IValueWrapper stringInstance =
+ new FormatAndParseTestingStringId(new StringValue(value.ToString()));
+ Assert.IsType(stringInstance.Serialize());
+ Assert.Equal(value.ToString(), stringInstance.Serialize()?.Value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Serialize_ToCoreType_ShouldReturnExpectedResult(int value)
+ {
+ IValueWrapper intInstance =
+ new FormatAndParseTestingIntId(value);
+ Assert.IsType(intInstance.Serialize());
+ Assert.Equal(value, intInstance.Serialize());
+
+ IValueWrapper stringInstance =
+ new FormatAndParseTestingStringId(new StringValue(value.ToString()));
+ Assert.IsType(stringInstance.Serialize());
+ Assert.Equal(value.ToString(), stringInstance.Serialize());
+ }
+
[Theory]
[InlineData(null)]
[InlineData(0)]
@@ -422,6 +514,41 @@ public void SerializeWithNewtonsoftJson_WithDecimal_ShouldReturnExpectedResult(i
Assert.Equal($@"""{value}""", Newtonsoft.Json.JsonConvert.SerializeObject((DecimalId?)value));
}
+ ///
+ /// Helper to access abstract statics.
+ ///
+ private static TWrapper Deserialize(TValue value)
+ where TWrapper : IValueWrapper
+ {
+ return TWrapper.Deserialize(value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Deserialize_FromImmediateUnderlyingType_ShouldReturnExpectedResult(int value)
+ {
+ var intInstance = new FormatAndParseTestingIntWrapper(value);
+ Assert.IsType(Deserialize(intInstance));
+ Assert.Equal(value, Deserialize(intInstance).Value?.Value.Value);
+
+ var stringInstance = new StringValue(value.ToString());
+ Assert.IsType(Deserialize(stringInstance));
+ Assert.Equal(value.ToString(), Deserialize(stringInstance).Value?.Value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Deserialize_FromCoreType_ShouldReturnExpectedResult(int value)
+ {
+ Assert.IsType(Deserialize(value));
+ Assert.Equal(value, Deserialize(value).Value?.Value.Value);
+
+ Assert.IsType(Deserialize(value.ToString()));
+ Assert.Equal(value.ToString(), Deserialize(value.ToString()).Value?.Value);
+ }
+
[Theory]
[InlineData("null", null)]
[InlineData("0", 0)]
@@ -779,7 +906,8 @@ internal readonly partial struct FullySelfImplementedIdentity
ISpanParsable,
IUtf8SpanFormattable,
IUtf8SpanParsable,
- ISerializableDomainObject
+ IDirectValueWrapper,
+ ICoreValueWrapper
{
public int Value { get; private init; }
@@ -796,7 +924,7 @@ public static FullySelfImplementedIdentity Create(int value)
///
/// Serializes a domain object as a plain value.
///
- int ISerializableDomainObject.Serialize()
+ int IValueWrapper.Serialize()
{
return this.Value;
}
@@ -804,7 +932,7 @@ int ISerializableDomainObject.Serialize()
///
/// Deserializes a plain value back into a domain object, without using a parameterized constructor.
///
- static FullySelfImplementedIdentity ISerializableDomainObject.Deserialize(int value)
+ static FullySelfImplementedIdentity IValueWrapper.Deserialize(int value)
{
return new FullySelfImplementedIdentity() { Value = value };
}
@@ -852,7 +980,7 @@ public override string ToString()
#region Formatting & Parsing
-#if !NET10_0_OR_GREATER // Starting from .NET 10, these operations are provided by default implementations and extension methods
+//#if !NET10_0_OR_GREATER // Starting from .NET 10, these operations are provided by default implementations and extension methods
public string ToString(string? format, IFormatProvider? formatProvider) =>
FormattingHelper.ToString(this.Value, format, formatProvider);
@@ -887,7 +1015,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provid
public static FullySelfImplementedIdentity Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) =>
(FullySelfImplementedIdentity)ParsingHelper.Parse(utf8Text, provider);
-#endif
+//#endif
#endregion
}
diff --git a/DomainModeling.Tests/WrapperValueObjectTests.cs b/DomainModeling.Tests/WrapperValueObjectTests.cs
index c34eec2..516548f 100644
--- a/DomainModeling.Tests/WrapperValueObjectTests.cs
+++ b/DomainModeling.Tests/WrapperValueObjectTests.cs
@@ -292,6 +292,98 @@ public void CastFromNullableUnderlyingType_Regularly_ShouldReturnExpectedResult(
Assert.Equal(expectedResult, result?.Value);
}
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Value_ViaCoreValueInterface_ShouldReturnExpectedResult(int value)
+ {
+ ICoreValueWrapper intInstance =
+ new FormatAndParseTestingIntWrapper(value);
+ Assert.IsType(intInstance.Value);
+ Assert.Equal(value, intInstance.Serialize());
+
+ ICoreValueWrapper stringInstance =
+ new FormatAndParseTestingStringWrapper(new FormatAndParseTestingNestedStringWrapper(new FormatAndParseTestingStringId(new StringValue(value.ToString()))));
+ Assert.IsType(stringInstance.Value);
+ Assert.Equal(value.ToString(), stringInstance.Value);
+ }
+
+ ///
+ /// Helper to access abstract statics.
+ ///
+ private static TWrapper CreateFromDirectUnderlyingValue(TValue value)
+ where TWrapper : IDirectValueWrapper
+ {
+ return TWrapper.Create(value);
+ }
+
+ ///
+ /// Helper to access abstract statics.
+ ///
+ private static TWrapper CreateFromCoreValue(TValue value)
+ where TWrapper : ICoreValueWrapper
+ {
+ return TWrapper.Create(value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Create_ViaDirectUnderlyingValueInterface_ShouldReturnExpectedResult(int value)
+ {
+ var intInstance = new IntId(value);
+ Assert.IsType(CreateFromDirectUnderlyingValue(intInstance));
+ Assert.Equal(value, CreateFromDirectUnderlyingValue(intInstance).Value.Value);
+
+ var stringInstance = new FormatAndParseTestingNestedStringWrapper(new FormatAndParseTestingStringId(new StringValue(value.ToString())));
+ Assert.IsType(CreateFromDirectUnderlyingValue(stringInstance));
+ Assert.Equal(value.ToString(), CreateFromDirectUnderlyingValue(stringInstance).Value.Value.Value?.Value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Create_ViaCoreValueInterface_ShouldReturnExpectedResult(int value)
+ {
+ Assert.IsType(CreateFromCoreValue(value));
+ Assert.Equal(value, CreateFromCoreValue(value).Value.Value);
+
+ Assert.IsType(CreateFromCoreValue(value.ToString()));
+ Assert.Equal(value.ToString(), CreateFromCoreValue(value.ToString()).Value.Value.Value?.Value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Serialize_ToImmediateUnderlyingType_ShouldReturnExpectedResult(int value)
+ {
+ IValueWrapper intInstance =
+ new FormatAndParseTestingIntWrapper(value);
+ Assert.IsType(intInstance.Serialize());
+ Assert.Equal(value, intInstance.Serialize().Value);
+
+ IValueWrapper stringInstance =
+ new FormatAndParseTestingStringWrapper(new FormatAndParseTestingNestedStringWrapper(new FormatAndParseTestingStringId(new StringValue(value.ToString()))));
+ Assert.IsType(stringInstance.Serialize());
+ Assert.Equal(value.ToString(), stringInstance.Serialize()?.Value.Value?.Value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Serialize_ToCoreType_ShouldReturnExpectedResult(int value)
+ {
+ IValueWrapper intInstance =
+ new FormatAndParseTestingIntWrapper(value);
+ Assert.IsType(intInstance.Serialize());
+ Assert.Equal(value, intInstance.Serialize());
+
+ IValueWrapper stringInstance =
+ new FormatAndParseTestingStringWrapper(new FormatAndParseTestingNestedStringWrapper(new FormatAndParseTestingStringId(new StringValue(value.ToString()))));
+ Assert.IsType(stringInstance.Serialize());
+ Assert.Equal(value.ToString(), stringInstance.Serialize());
+ }
+
[Theory]
[InlineData(null)]
[InlineData(0)]
@@ -363,6 +455,41 @@ public void SerializeWithNewtonsoftJson_WithDecimal_ShouldReturnExpectedResult(i
Assert.Equal(value is null ? "null" : $"{value}.0", Newtonsoft.Json.JsonConvert.SerializeObject(instance));
}
+ ///
+ /// Helper to access abstract statics.
+ ///
+ private static TWrapper Deserialize(TValue value)
+ where TWrapper : IValueWrapper
+ {
+ return TWrapper.Deserialize(value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Deserialize_FromImmediateUnderlyingType_ShouldReturnExpectedResult(int value)
+ {
+ var intInstance = new IntId(value);
+ Assert.IsType(Deserialize(intInstance));
+ Assert.Equal(value, Deserialize(intInstance).Value.Value);
+
+ var stringInstance = new FormatAndParseTestingNestedStringWrapper(new FormatAndParseTestingStringId(new StringValue(value.ToString())));
+ Assert.IsType(Deserialize(stringInstance));
+ Assert.Equal(value.ToString(), Deserialize(stringInstance).Value.Value.Value?.Value);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Deserialize_FromCoreType_ShouldReturnExpectedResult(int value)
+ {
+ Assert.IsType(Deserialize(value));
+ Assert.Equal(value, Deserialize(value).Value.Value);
+
+ Assert.IsType(Deserialize(value.ToString()));
+ Assert.Equal(value.ToString(), Deserialize(value.ToString()).Value.Value.Value?.Value);
+ }
+
[Theory]
[InlineData("null", null)]
[InlineData("0", 0)]
@@ -798,13 +925,14 @@ public bool TryFormat(Span utf8Destination, out int bytesWritten, ReadOnly
[Newtonsoft.Json.JsonConverter(typeof(ValueWrapperNewtonsoftJsonConverter))]
internal sealed partial class FullySelfImplementedWrapperValueObject
: WrapperValueObject,
- IValueWrapper,
+ IEquatable,
IComparable,
ISpanFormattable,
ISpanParsable,
IUtf8SpanFormattable,
IUtf8SpanParsable,
- ISerializableDomainObject
+ IDirectValueWrapper,
+ ICoreValueWrapper
{
protected sealed override StringComparison StringComparison => throw new NotSupportedException("This operation applies to string-based value objects only.");
@@ -828,7 +956,7 @@ static FullySelfImplementedWrapperValueObject IValueWrapper
/// Serializes a domain object as a plain value.
///
- int ISerializableDomainObject.Serialize()
+ int IValueWrapper.Serialize()
{
return this.Value;
}
@@ -836,7 +964,7 @@ int ISerializableDomainObject.Seria
///
/// Deserializes a plain value back into a domain object, without using a parameterized constructor.
///
- static FullySelfImplementedWrapperValueObject ISerializableDomainObject.Deserialize(int value)
+ static FullySelfImplementedWrapperValueObject IValueWrapper.Deserialize(int value)
{
#pragma warning disable IDE0079 // Remove unnecessary suppression -- Suppression below is falsely flagged as unnecessary
#pragma warning disable CS0618 // Obsolete constructor is intended for us
@@ -845,6 +973,12 @@ static FullySelfImplementedWrapperValueObject ISerializableDomainObject.Value => (long)this.Value;
+ static FullySelfImplementedWrapperValueObject IValueWrapper