From 6cbf03ad11635e0c5549f7fb53983e012bbd78ca Mon Sep 17 00:00:00 2001
From: Timovzl <655426+Timovzl@users.noreply.github.com>
Date: Fri, 22 Dec 2023 16:47:22 +0100
Subject: [PATCH 1/6] Implemented major version 3.0.0.
---
DomainModeling.Example/CharacterSet.cs | 6 +-
DomainModeling.Example/Color.cs | 10 +-
DomainModeling.Example/Description.cs | 6 +-
.../DomainModeling.Example.csproj | 3 +-
DomainModeling.Example/PaymentDummyBuilder.cs | 4 +-
.../AssemblyInspectionExtensions.cs | 40 ++
.../Common/SimpleLocation.cs | 34 ++
.../Common/StructuralList.cs | 55 ++
...ModelConfiguratorGenerator.DomainEvents.cs | 48 ++
...mainModelConfiguratorGenerator.Entities.cs | 48 ++
...inModelConfiguratorGenerator.Identities.cs | 49 ++
...nfiguratorGenerator.WrapperValueObjects.cs | 49 ++
.../DomainModelConfiguratorGenerator.cs | 14 +
.../EntityFrameworkConfigurationGenerator.cs | 435 ++++++++++++++
DomainModeling.Generator/Constants.cs | 8 +-
.../DomainEventGenerator.cs | 112 ++++
.../DomainModeling.Generator.csproj | 8 +-
.../DummyBuilderGenerator.cs | 192 +++---
DomainModeling.Generator/EntityGenerator.cs | 112 ++++
DomainModeling.Generator/IGeneratable.cs | 27 +-
DomainModeling.Generator/IdentityGenerator.cs | 558 +++++++++++-------
.../JsonSerializationGenerator.cs | 154 +++++
.../SourceGeneratedAttributeAnalyzer.cs | 94 ---
DomainModeling.Generator/SourceGenerator.cs | 9 +-
.../SourceProductionContextExtensions.cs | 13 +-
DomainModeling.Generator/StringExtensions.cs | 25 +-
.../TypeDeclarationSyntaxExtensions.cs | 14 +-
.../TypeSymbolExtensions.cs | 111 ++--
.../TypeSyntaxExtensions.cs | 49 +-
.../ValueObjectGenerator.cs | 143 +++--
.../WrapperValueObjectGenerator.cs | 524 +++++++++++-----
.../Common/StructuralListTests.cs | 68 +++
.../Comparisons/EnumerableComparerTests.cs | 4 +-
.../Comparisons/LookupComparerTests.cs | 22 +-
.../DomainModeling.Tests.csproj | 12 +-
DomainModeling.Tests/DummyBuilderTests.cs | 29 +-
...ityFrameworkConfigurationGeneratorTests.cs | 212 +++++++
.../FileScopedNamespaceTests.cs | 16 +-
DomainModeling.Tests/IdentityTests.cs | 404 ++++++++++---
DomainModeling.Tests/ValueObjectTests.cs | 64 +-
.../WrapperValueObjectTests.cs | 434 ++++++++++++--
.../Attributes/DomainEventAttribute.cs | 18 +
.../Attributes/DummyBuilderAttribute.cs | 27 +
DomainModeling/Attributes/EntityAttribute.cs | 18 +
.../IdentityValueObjectAttribute.cs | 21 +
.../SourceGeneratedAttribute.cs | 29 +-
.../Attributes/ValueObjectAttribute.cs | 18 +
.../Attributes/WrapperValueObjectAttribute.cs | 21 +
.../Comparisons/EnumerableComparer.cs | 2 -
.../Configuration/IDomainEventConfigurator.cs | 23 +
.../Configuration/IEntityConfigurator.cs | 23 +
.../Configuration/IIdentityConfigurator.cs | 24 +
.../IWrapperValueObjectConfigurator.cs | 24 +
.../Conversions/DomainObjectSerializer.cs | 186 ++++++
.../Conversions/FormattingHelper.cs | 179 ++++++
.../Conversions/ObjectInstantiator.cs | 55 ++
DomainModeling/Conversions/ParsingHelper.cs | 175 ++++++
.../Conversions/Utf8JsonReaderExtensions.cs | 38 +-
DomainModeling/DomainModeling.csproj | 36 +-
DomainModeling/DummyBuilder.cs | 1 +
DomainModeling/Entity.cs | 34 +-
DomainModeling/ISerializableDomainObject.cs | 23 +
DomainModeling/IWrapperValueObject.cs | 17 +
DomainModeling/WrapperValueObject.cs | 2 +-
README.md | 274 +++++++--
65 files changed, 4514 insertions(+), 973 deletions(-)
create mode 100644 DomainModeling.Generator/AssemblyInspectionExtensions.cs
create mode 100644 DomainModeling.Generator/Common/SimpleLocation.cs
create mode 100644 DomainModeling.Generator/Common/StructuralList.cs
create mode 100644 DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.DomainEvents.cs
create mode 100644 DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Entities.cs
create mode 100644 DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Identities.cs
create mode 100644 DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.WrapperValueObjects.cs
create mode 100644 DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.cs
create mode 100644 DomainModeling.Generator/Configurators/EntityFrameworkConfigurationGenerator.cs
create mode 100644 DomainModeling.Generator/DomainEventGenerator.cs
create mode 100644 DomainModeling.Generator/EntityGenerator.cs
create mode 100644 DomainModeling.Generator/JsonSerializationGenerator.cs
delete mode 100644 DomainModeling.Generator/SourceGeneratedAttributeAnalyzer.cs
create mode 100644 DomainModeling.Tests/Common/StructuralListTests.cs
create mode 100644 DomainModeling.Tests/EntityFramework/EntityFrameworkConfigurationGeneratorTests.cs
create mode 100644 DomainModeling/Attributes/DomainEventAttribute.cs
create mode 100644 DomainModeling/Attributes/DummyBuilderAttribute.cs
create mode 100644 DomainModeling/Attributes/EntityAttribute.cs
create mode 100644 DomainModeling/Attributes/IdentityValueObjectAttribute.cs
rename DomainModeling/{ => Attributes}/SourceGeneratedAttribute.cs (64%)
create mode 100644 DomainModeling/Attributes/ValueObjectAttribute.cs
create mode 100644 DomainModeling/Attributes/WrapperValueObjectAttribute.cs
create mode 100644 DomainModeling/Configuration/IDomainEventConfigurator.cs
create mode 100644 DomainModeling/Configuration/IEntityConfigurator.cs
create mode 100644 DomainModeling/Configuration/IIdentityConfigurator.cs
create mode 100644 DomainModeling/Configuration/IWrapperValueObjectConfigurator.cs
create mode 100644 DomainModeling/Conversions/DomainObjectSerializer.cs
create mode 100644 DomainModeling/Conversions/FormattingHelper.cs
create mode 100644 DomainModeling/Conversions/ObjectInstantiator.cs
create mode 100644 DomainModeling/Conversions/ParsingHelper.cs
create mode 100644 DomainModeling/ISerializableDomainObject.cs
create mode 100644 DomainModeling/IWrapperValueObject.cs
diff --git a/DomainModeling.Example/CharacterSet.cs b/DomainModeling.Example/CharacterSet.cs
index c668b02..64b54fd 100644
--- a/DomainModeling.Example/CharacterSet.cs
+++ b/DomainModeling.Example/CharacterSet.cs
@@ -3,12 +3,12 @@ namespace Architect.DomainModeling.Example;
///
/// Demonstrates structural equality with collections.
///
-[SourceGenerated]
-public partial class CharacterSet : ValueObject
+[ValueObject]
+public partial class CharacterSet
{
public override string ToString() => $"[{String.Join(", ", this.Characters)}]";
- public IReadOnlySet Characters { get; }
+ public IReadOnlySet Characters { get; private init; }
public CharacterSet(IEnumerable characters)
{
diff --git a/DomainModeling.Example/Color.cs b/DomainModeling.Example/Color.cs
index e528721..c08523c 100644
--- a/DomainModeling.Example/Color.cs
+++ b/DomainModeling.Example/Color.cs
@@ -2,16 +2,16 @@ namespace Architect.DomainModeling.Example;
// Use "Go To Definition" on the type to view the source-generated partial
// Uncomment the IComparable interface to see how the generated code changes
-[SourceGenerated]
-public partial class Color : ValueObject//, IComparable
+[ValueObject]
+public partial class Color //: IComparable
{
public static Color RedColor { get; } = new Color(red: UInt16.MaxValue, green: 0, blue: 0);
public static Color GreenColor { get; } = new Color(red: 0, green: UInt16.MaxValue, blue: 0);
public static Color BlueColor { get; } = new Color(red: 0, green: 0, blue: UInt16.MaxValue);
- public ushort Red { get; }
- public ushort Green { get; }
- public ushort Blue { get; }
+ public ushort Red { get; private init; }
+ public ushort Green { get; private init; }
+ public ushort Blue { get; private init; }
public Color(ushort red, ushort green, ushort blue)
{
diff --git a/DomainModeling.Example/Description.cs b/DomainModeling.Example/Description.cs
index e375640..f4b6cb5 100644
--- a/DomainModeling.Example/Description.cs
+++ b/DomainModeling.Example/Description.cs
@@ -2,15 +2,15 @@ namespace Architect.DomainModeling.Example;
// Use "Go To Definition" on the type to view the source-generated partial
// Uncomment the IComparable interface to see how the generated code changes
-[SourceGenerated]
-public partial class Description : WrapperValueObject//, IComparable
+[WrapperValueObject]
+public partial class Description //: IComparable
{
// For string wrappers, we must define how they are compared
protected override StringComparison StringComparison => StringComparison.OrdinalIgnoreCase;
// Any component that we define manually is omitted by the generated code
// For example, we can explicitly define the Value property to have greater clarity, since it is quintessential
- public string Value { get; }
+ public string Value { get; private init; }
// An explicitly defined constructor allows us to enforce the domain rules and invariants
public Description(string value)
diff --git a/DomainModeling.Example/DomainModeling.Example.csproj b/DomainModeling.Example/DomainModeling.Example.csproj
index 915f544..e8e246e 100644
--- a/DomainModeling.Example/DomainModeling.Example.csproj
+++ b/DomainModeling.Example/DomainModeling.Example.csproj
@@ -2,13 +2,14 @@
Exe
- net7.0
+ net6.0
Architect.DomainModeling.Example
Architect.DomainModeling.Example
Enable
Enable
False
True
+ 12
diff --git a/DomainModeling.Example/PaymentDummyBuilder.cs b/DomainModeling.Example/PaymentDummyBuilder.cs
index 2931219..532ee3f 100644
--- a/DomainModeling.Example/PaymentDummyBuilder.cs
+++ b/DomainModeling.Example/PaymentDummyBuilder.cs
@@ -1,8 +1,8 @@
namespace Architect.DomainModeling.Example;
// The source-generated partial provides an appropriate type summary
-[SourceGenerated]
-public sealed partial class PaymentDummyBuilder : DummyBuilder
+[DummyBuilder]
+public sealed partial class PaymentDummyBuilder
{
// The source-generated partial defines a default value for each property, along with a fluent method to change it
diff --git a/DomainModeling.Generator/AssemblyInspectionExtensions.cs b/DomainModeling.Generator/AssemblyInspectionExtensions.cs
new file mode 100644
index 0000000..bdf5b05
--- /dev/null
+++ b/DomainModeling.Generator/AssemblyInspectionExtensions.cs
@@ -0,0 +1,40 @@
+using Microsoft.CodeAnalysis;
+
+namespace Architect.DomainModeling.Generator;
+
+///
+/// Provides extension methods that help inspect assemblies.
+///
+public static class AssemblyInspectionExtensions
+{
+ ///
+ /// Enumerates the given and all of its referenced instances, recursively.
+ /// Does not deduplicate.
+ ///
+ /// A predicate that can filter out assemblies and prevent further recursion into them.
+ public static IEnumerable EnumerateAssembliesRecursively(this IAssemblySymbol assemblySymbol, Func? predicate = null)
+ {
+ if (predicate is not null && !predicate(assemblySymbol))
+ yield break;
+
+ yield return assemblySymbol;
+
+ foreach (var module in assemblySymbol.Modules)
+ foreach (var assembly in module.ReferencedAssemblySymbols)
+ foreach (var nestedAssembly in EnumerateAssembliesRecursively(assembly, predicate))
+ yield return nestedAssembly;
+ }
+
+ ///
+ /// Enumerates all non-nested types in the given , recursively.
+ ///
+ public static IEnumerable EnumerateNonNestedTypes(this INamespaceSymbol namespaceSymbol)
+ {
+ foreach (var type in namespaceSymbol.GetTypeMembers())
+ yield return type;
+
+ foreach (var childNamespace in namespaceSymbol.GetNamespaceMembers())
+ foreach (var type in EnumerateNonNestedTypes(childNamespace))
+ yield return type;
+ }
+}
diff --git a/DomainModeling.Generator/Common/SimpleLocation.cs b/DomainModeling.Generator/Common/SimpleLocation.cs
new file mode 100644
index 0000000..bf3123b
--- /dev/null
+++ b/DomainModeling.Generator/Common/SimpleLocation.cs
@@ -0,0 +1,34 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Architect.DomainModeling.Generator.Common;
+
+///
+/// Represents a as a simple, serializable structure.
+///
+internal sealed record class SimpleLocation
+{
+ public string FilePath { get; }
+ public TextSpan TextSpan { get; }
+ public LinePositionSpan LineSpan { get; }
+
+ public SimpleLocation(Location location)
+ {
+ var lineSpan = location.GetLineSpan();
+ this.FilePath = lineSpan.Path;
+ this.TextSpan = location.SourceSpan;
+ this.LineSpan = lineSpan.Span;
+ }
+
+ public SimpleLocation(string filePath, TextSpan textSpan, LinePositionSpan lineSpan)
+ {
+ this.FilePath = filePath;
+ this.TextSpan = textSpan;
+ this.LineSpan = lineSpan;
+ }
+
+#nullable disable
+ public static implicit operator SimpleLocation(Location location) => location is null ? null : new SimpleLocation(location);
+ public static implicit operator Location(SimpleLocation location) => location is null ? null : Location.Create(location.FilePath, location.TextSpan, location.LineSpan);
+#nullable enable
+}
diff --git a/DomainModeling.Generator/Common/StructuralList.cs b/DomainModeling.Generator/Common/StructuralList.cs
new file mode 100644
index 0000000..db380ae
--- /dev/null
+++ b/DomainModeling.Generator/Common/StructuralList.cs
@@ -0,0 +1,55 @@
+namespace Architect.DomainModeling.Generator.Common;
+
+///
+/// Wraps an in a wrapper with structural equality using the collection's elements.
+///
+/// The type of the collection to wrap.
+/// The type of the collection's elements.
+internal sealed class StructuralList(
+ TCollection value)
+ : IEquatable>
+ where TCollection : IReadOnlyList
+{
+ public TCollection Value { get; } = value ?? throw new ArgumentNullException(nameof(value));
+
+ public override int GetHashCode() => this.Value is TCollection value && value.Count > 0
+ ? CombineHashCodes(
+ value.Count,
+ value[0]?.GetHashCode() ?? 0,
+ value[value.Count - 1]?.GetHashCode() ?? 0)
+ : 0;
+ public override bool Equals(object obj) => obj is StructuralList other && this.Equals(other);
+
+ public bool Equals(StructuralList other)
+ {
+ if (other is null)
+ return false;
+
+ var left = this.Value;
+ var right = other.Value;
+
+ if (right.Count != left.Count)
+ return false;
+
+ for (var i = 0; i < left.Count; i++)
+ if (left[i] is not TElement leftElement ? right[i] is not null : !leftElement.Equals(right[i]))
+ return false;
+
+ return true;
+ }
+
+ private static int CombineHashCodes(int count, int firstHashCode, int lastHashCode)
+ {
+ var countInHighBits = (ulong)count << 16;
+
+ // In the upper half, combine the count with the first hash code
+ // In the lower half, combine the count with the last hash code
+ var combined = ((ulong)firstHashCode ^ countInHighBits) << 33; // Offset by 1 additional bit, because UInt64.GetHashCode() XORs its halves, which would cause 0 for identical first and last (e.g. single element)
+ combined |= (ulong)lastHashCode ^ countInHighBits;
+
+ return combined.GetHashCode();
+ }
+
+ public static implicit operator TCollection(StructuralList instance) => instance.Value;
+ public static implicit operator StructuralList(TCollection value) => new StructuralList(value);
+}
diff --git a/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.DomainEvents.cs b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.DomainEvents.cs
new file mode 100644
index 0000000..20407c7
--- /dev/null
+++ b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.DomainEvents.cs
@@ -0,0 +1,48 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+
+namespace Architect.DomainModeling.Generator.Configurators;
+
+public partial class DomainModelConfiguratorGenerator
+{
+ internal static void GenerateSourceForDomainEvents(SourceProductionContext context, (ImmutableArray Generatables, (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)
+ return;
+
+ var targetNamespace = input.Metadata.AssemblyName;
+
+ var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.Generatables.Select(generatable =>
+ $"configurator.ConfigureDomainEvent<{generatable.ContainingNamespace}.{generatable.TypeName}>({Environment.NewLine} new {Constants.DomainModelingNamespace}.Configuration.IDomainEventConfigurator.Args() {{ HasDefaultConstructor = {(generatable.ExistingComponents.HasFlag(DomainEventGenerator.DomainEventTypeComponents.DefaultConstructor) ? "true" : "false")} }});"));
+
+ var source = $@"
+using {Constants.DomainModelingNamespace};
+
+#nullable enable
+
+namespace {targetNamespace}
+{{
+ public static class DomainEventDomainModelConfigurator
+ {{
+ ///
+ ///
+ /// Invokes a callback on the given for each marked domain event type in the current assembly.
+ ///
+ ///
+ /// For example, this can be used to have Entity Framework configure a convention for every matching type in the domain model, in a trim-safe way.
+ ///
+ ///
+ public static void ConfigureDomainEvents({Constants.DomainModelingNamespace}.Configuration.IDomainEventConfigurator configurator)
+ {{
+ {configurationText}
+ }}
+ }}
+}}
+";
+
+ AddSource(context, source, "DomainEventDomainModelConfigurator", targetNamespace);
+ }
+}
diff --git a/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Entities.cs b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Entities.cs
new file mode 100644
index 0000000..81d4bcd
--- /dev/null
+++ b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Entities.cs
@@ -0,0 +1,48 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+
+namespace Architect.DomainModeling.Generator.Configurators;
+
+public partial class DomainModelConfiguratorGenerator
+{
+ internal static void GenerateSourceForEntities(SourceProductionContext context, (ImmutableArray Generatables, (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)
+ return;
+
+ var targetNamespace = input.Metadata.AssemblyName;
+
+ var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.Generatables.Select(generatable =>
+ $"configurator.ConfigureEntity<{generatable.ContainingNamespace}.{generatable.TypeName}>({Environment.NewLine} new {Constants.DomainModelingNamespace}.Configuration.IEntityConfigurator.Args() {{ HasDefaultConstructor = {(generatable.ExistingComponents.HasFlag(EntityGenerator.EntityTypeComponents.DefaultConstructor) ? "true" : "false")} }});"));
+
+ var source = $@"
+using {Constants.DomainModelingNamespace};
+
+#nullable enable
+
+namespace {targetNamespace}
+{{
+ public static class EntityDomainModelConfigurator
+ {{
+ ///
+ ///
+ /// Invokes a callback on the given for each marked type in the current assembly.
+ ///
+ ///
+ /// For example, this can be used to have Entity Framework configure a convention for every matching type in the domain model, in a trim-safe way.
+ ///
+ ///
+ public static void ConfigureEntities({Constants.DomainModelingNamespace}.Configuration.IEntityConfigurator configurator)
+ {{
+ {configurationText}
+ }}
+ }}
+}}
+";
+
+ AddSource(context, source, "EntityDomainModelConfigurator", targetNamespace);
+ }
+}
diff --git a/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Identities.cs b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Identities.cs
new file mode 100644
index 0000000..564222a
--- /dev/null
+++ b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Identities.cs
@@ -0,0 +1,49 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+
+namespace Architect.DomainModeling.Generator.Configurators;
+
+public partial class DomainModelConfiguratorGenerator
+{
+ internal static void GenerateSourceForIdentities(SourceProductionContext context, (ImmutableArray Generatables, (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)
+ return;
+
+ var targetNamespace = input.Metadata.AssemblyName;
+
+ var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.Generatables.Select(generatable => $"""
+ configurator.ConfigureIdentity<{generatable.ContainingNamespace}.{generatable.IdTypeName}, {generatable.UnderlyingTypeFullyQualifiedName}>({Environment.NewLine} new {Constants.DomainModelingNamespace}.Configuration.IIdentityConfigurator.Args());
+ """));
+
+ var source = $@"
+using {Constants.DomainModelingNamespace};
+
+#nullable enable
+
+namespace {targetNamespace}
+{{
+ public static class IdentityDomainModelConfigurator
+ {{
+ ///
+ ///
+ /// Invokes a callback on the given for each marked type in the current assembly.
+ ///
+ ///
+ /// For example, this can be used to have Entity Framework configure a convention for every matching type in the domain model, in a trim-safe way.
+ ///
+ ///
+ public static void ConfigureIdentities({Constants.DomainModelingNamespace}.Configuration.IIdentityConfigurator configurator)
+ {{
+ {configurationText}
+ }}
+ }}
+}}
+";
+
+ AddSource(context, source, "IdentityDomainModelConfigurator", targetNamespace);
+ }
+}
diff --git a/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.WrapperValueObjects.cs b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.WrapperValueObjects.cs
new file mode 100644
index 0000000..ca0a471
--- /dev/null
+++ b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.WrapperValueObjects.cs
@@ -0,0 +1,49 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+
+namespace Architect.DomainModeling.Generator.Configurators;
+
+public partial class DomainModelConfiguratorGenerator
+{
+ internal static void GenerateSourceForWrapperValueObjects(SourceProductionContext context, (ImmutableArray Generatables, (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)
+ 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 {Constants.DomainModelingNamespace}.Configuration.IWrapperValueObjectConfigurator.Args());
+ """));
+
+ var source = $@"
+using {Constants.DomainModelingNamespace};
+
+#nullable enable
+
+namespace {targetNamespace}
+{{
+ public static class WrapperValueObjectDomainModelConfigurator
+ {{
+ ///
+ ///
+ /// Invokes a callback on the given for each marked type in the current assembly.
+ ///
+ ///
+ /// For example, this can be used to have Entity Framework configure a convention for every matching type in the domain model, in a trim-safe way.
+ ///
+ ///
+ public static void ConfigureWrapperValueObjects({Constants.DomainModelingNamespace}.Configuration.IWrapperValueObjectConfigurator configurator)
+ {{
+ {configurationText}
+ }}
+ }}
+}}
+";
+
+ AddSource(context, source, "WrapperValueObjectDomainModelConfigurator", targetNamespace);
+ }
+}
diff --git a/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.cs b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.cs
new file mode 100644
index 0000000..43c003c
--- /dev/null
+++ b/DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.cs
@@ -0,0 +1,14 @@
+using Microsoft.CodeAnalysis;
+
+namespace Architect.DomainModeling.Generator.Configurators;
+
+///
+/// Generates DomainModelConfigurators, types intended to configure miscellaneous components when it comes to certain types of domain objects.
+///
+public partial class DomainModelConfiguratorGenerator : SourceGenerator
+{
+ public override void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Only invoked from other generators
+ }
+}
diff --git a/DomainModeling.Generator/Configurators/EntityFrameworkConfigurationGenerator.cs b/DomainModeling.Generator/Configurators/EntityFrameworkConfigurationGenerator.cs
new file mode 100644
index 0000000..ba10c3c
--- /dev/null
+++ b/DomainModeling.Generator/Configurators/EntityFrameworkConfigurationGenerator.cs
@@ -0,0 +1,435 @@
+using System.Collections.Immutable;
+using Architect.DomainModeling.Generator.Common;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Architect.DomainModeling.Generator.Configurators;
+
+[Generator]
+public partial class EntityFrameworkConfigurationGenerator : SourceGenerator
+{
+ ///
+ /// A value provider that returns a single boolean value indicating whether EF's ConfigureConventions() is being called.
+ ///
+ internal static IncrementalValueProvider CreateHasConfigureConventionsValueProvider(IncrementalGeneratorInitializationContext context)
+ {
+ var result = context.SyntaxProvider.CreateSyntaxProvider(FilterSyntaxNode, IsConfigureConventions)
+ .Collect()
+ .Select((bools, _) => bools.Any());
+
+ return result;
+ }
+
+ internal static IncrementalValueProvider<(bool HasConfigureConventions, string AssemblyName)> CreateMetadataProvider(IncrementalGeneratorInitializationContext context)
+ {
+ var hasConfigureConventionsProvider = CreateHasConfigureConventionsValueProvider(context);
+ var assemblyNameProvider = context.CompilationProvider.Select((compilation, _) => compilation.AssemblyName ?? compilation.Assembly.Name);
+ var result = hasConfigureConventionsProvider.Combine(assemblyNameProvider);
+ return result;
+ }
+
+ public override void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var assemblyNameProvider = context.CompilationProvider.Select((compilation, _) => compilation.AssemblyName ?? compilation.Assembly.Name);
+
+ var hasConfigureConventionsProvider = CreateHasConfigureConventionsValueProvider(context);
+
+ var assembliesContainingDomainModelConfiguratorsProvider = context.CompilationProvider
+ .Combine(hasConfigureConventionsProvider)
+ .Select(GetAssembliesContainingDomainModelConfigurators);
+
+ context.RegisterSourceOutput(assembliesContainingDomainModelConfiguratorsProvider.Combine(assemblyNameProvider), GenerateSource);
+ }
+
+ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancellationToken = default)
+ {
+ // Detect EF's presence by the ConfigureConventions() method, which is needed to use our extensions anyway
+ if (node is MethodDeclarationSyntax mds && mds.Identifier.ValueText == "ConfigureConventions")
+ return true;
+
+ return false;
+ }
+
+ private static bool IsConfigureConventions(GeneratorSyntaxContext context, CancellationToken _)
+ {
+ if (context.Node is MethodDeclarationSyntax mds &&
+ context.SemanticModel.GetDeclaredSymbol(mds) is IMethodSymbol methodSymbol &&
+ methodSymbol.Name == "ConfigureConventions" &&
+ methodSymbol.IsOverride &&
+ methodSymbol.Parameters.Length == 1 &&
+ methodSymbol.Parameters[0].Type.IsType("ModelConfigurationBuilder", "Microsoft.EntityFrameworkCore"))
+ return true;
+
+ return false;
+ }
+
+ private static Generatable GetAssembliesContainingDomainModelConfigurators((Compilation Compilation, bool HasConfigureConventions) input, CancellationToken _)
+ {
+ if (!input.HasConfigureConventions)
+ return new Generatable();
+
+ var ownAssemblyNamePrefix = input.Compilation.Assembly.Name;
+ ownAssemblyNamePrefix = ownAssemblyNamePrefix.Substring(0, ownAssemblyNamePrefix.IndexOf('.') is int dotIndex and > 0 ? dotIndex : ownAssemblyNamePrefix.Length);
+
+ var assembliesContainingIdentityConfigurator = new HashSet() { input.Compilation.Assembly.Name };
+ var assembliesContainingWrapperValueObjectConfigurator = new HashSet() { input.Compilation.Assembly.Name };
+ var assembliesContainingEntityConfigurator = new HashSet() { input.Compilation.Assembly.Name };
+ var assembliesContainingDomainEventConfigurator = new HashSet() { input.Compilation.Assembly.Name };
+
+ // Only consider referenced assemblies as long as they have the same top-level assembly name as the current one
+ foreach (var assembly in input.Compilation.Assembly.EnumerateAssembliesRecursively(assembly => assembly.Name.StartsWith(ownAssemblyNamePrefix, StringComparison.Ordinal)))
+ {
+ // Although technically the type name is not a definitive indication, the simplicity of this check saves us a lot of work
+ // Also, the type names are relatively specific
+ foreach (var typeName in assembly.TypeNames)
+ {
+ switch (typeName)
+ {
+ case "IdentityDomainModelConfigurator":
+ assembliesContainingIdentityConfigurator.Add(assembly.Name);
+ break;
+ case "WrapperValueObjectDomainModelConfigurator":
+ assembliesContainingWrapperValueObjectConfigurator.Add(assembly.Name);
+ break;
+ case "EntityDomainModelConfigurator":
+ assembliesContainingEntityConfigurator.Add(assembly.Name);
+ break;
+ case "DomainEventDomainModelConfigurator":
+ assembliesContainingDomainEventConfigurator.Add(assembly.Name);
+ break;
+ }
+ }
+ }
+
+ var result = new Generatable()
+ {
+ UsesEntityFrameworkConventions = true,
+ ReferencedAssembliesWithIdentityConfigurator = assembliesContainingIdentityConfigurator.OrderBy(name => name).ToImmutableArray(),
+ ReferencedAssembliesWithWrapperValueObjectConfigurator = assembliesContainingWrapperValueObjectConfigurator.OrderBy(name => name).ToImmutableArray(),
+ ReferencedAssembliesWithEntityConfigurator = assembliesContainingEntityConfigurator.OrderBy(name => name).ToImmutableArray(),
+ ReferencedAssembliesWithDomainEventConfigurator = assembliesContainingEntityConfigurator.OrderBy(name => name).ToImmutableArray(),
+ };
+ return result;
+ }
+
+ private static void GenerateSource(SourceProductionContext context, (Generatable Generatable, string AssemblyName) input)
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+
+ if (!input.Generatable.UsesEntityFrameworkConventions)
+ return;
+
+ var ownAssemblyName = input.AssemblyName;
+
+ var identityConfigurationCalls = String.Join(
+ $"{Environment.NewLine}\t\t\t",
+ input.Generatable.ReferencedAssembliesWithIdentityConfigurator!.Value.Select(assemblyName => $"{assemblyName}.IdentityDomainModelConfigurator.ConfigureIdentities(concreteConfigurator);"));
+ var wrapperValueObjectConfigurationCalls = String.Join(
+ $"{Environment.NewLine}\t\t\t",
+ input.Generatable.ReferencedAssembliesWithWrapperValueObjectConfigurator!.Value.Select(assemblyName => $"{assemblyName}.WrapperValueObjectDomainModelConfigurator.ConfigureWrapperValueObjects(concreteConfigurator);"));
+ var entityConfigurationCalls = String.Join(
+ $"{Environment.NewLine}\t\t\t",
+ input.Generatable.ReferencedAssembliesWithEntityConfigurator!.Value.Select(assemblyName => $"{assemblyName}.EntityDomainModelConfigurator.ConfigureEntities(concreteConfigurator);"));
+ var domainEventConfigurationCalls = String.Join(
+ $"{Environment.NewLine}\t\t\t",
+ input.Generatable.ReferencedAssembliesWithDomainEventConfigurator!.Value.Select(assemblyName => $"{assemblyName}.DomainEventDomainModelConfigurator.ConfigureDomainEvents(concreteConfigurator);"));
+
+ var source = $@"
+#if NET7_0_OR_GREATER
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using {Constants.DomainModelingNamespace};
+using {Constants.DomainModelingNamespace}.Conversions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Microsoft.EntityFrameworkCore.Metadata.Conventions;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable enable
+
+namespace {ownAssemblyName}
+{{
+ public static class EntityFrameworkDomainModelConfigurationExtensions
+ {{
+ ///
+ /// Allows conventions to be configured for domain objects.
+ /// Use the extension methods on the delegate parameter to enable specific conventions.
+ ///
+ public static ModelConfigurationBuilder ConfigureDomainModelConventions(this ModelConfigurationBuilder configurationBuilder, Action domainModel)
+ {{
+ domainModel(new DomainModelConfigurator(configurationBuilder));
+ return configurationBuilder;
+ }}
+
+ ///
+ ///
+ /// Configures conventions for all marked types.
+ ///
+ ///
+ /// This configures conversions to and from the underlying type for properties of the identity types.
+ /// It similarly configures the default type mapping for those types, which is used when queries encounter a type outside the context of a property, such as in CAST(), SUM(), AVG(), etc.
+ ///
+ ///
+ /// Additionally, -backed identities receive a mapping hint to use precision 28 and scale 0, a useful default for DistributedIds.
+ ///
+ ///
+ public static IDomainModelConfigurator ConfigureIdentityConventions(this IDomainModelConfigurator configurator)
+ {{
+ var concreteConfigurator = new EntityFrameworkIdentityConfigurator(configurator.ConfigurationBuilder);
+
+ {identityConfigurationCalls}
+
+ return configurator;
+ }}
+
+ ///
+ ///
+ /// Configures conventions for all marked types.
+ ///
+ ///
+ /// This configures conversions to and from the underlying type for properties of the wrapper types.
+ /// It similarly configures the default type mapping for those types, which is used when queries encounter a type outside the context of a property, such as in CAST(), SUM(), AVG(), etc.
+ ///
+ ///
+ public static IDomainModelConfigurator ConfigureWrapperValueObjectConventions(this IDomainModelConfigurator configurator)
+ {{
+ var concreteConfigurator = new EntityFrameworkWrapperValueObjectConfigurator(configurator.ConfigurationBuilder);
+
+ {wrapperValueObjectConfigurationCalls}
+
+ return configurator;
+ }}
+
+ ///
+ ///
+ /// Configures conventions for all marked types.
+ ///
+ ///
+ /// This configures instantiation without the use of constructors.
+ ///
+ ///
+ public static IDomainModelConfigurator ConfigureEntityConventions(this IDomainModelConfigurator configurator)
+ {{
+ EntityFrameworkEntityConfigurator concreteConfigurator = null!;
+ concreteConfigurator = new EntityFrameworkEntityConfigurator(() =>
+ {{
+ {entityConfigurationCalls}
+ }});
+
+ configurator.ConfigurationBuilder.Conventions.Add(_ => concreteConfigurator);
+
+ return configurator;
+ }}
+
+ ///
+ ///
+ /// Configures conventions for all marked domain event types.
+ ///
+ ///
+ /// This configures instantiation without the use of constructors.
+ ///
+ ///
+ public static IDomainModelConfigurator ConfigureDomainEventConventions(this IDomainModelConfigurator configurator)
+ {{
+ EntityFrameworkEntityConfigurator concreteConfigurator = null!;
+ concreteConfigurator = new EntityFrameworkEntityConfigurator(() =>
+ {{
+ {domainEventConfigurationCalls}
+ }});
+
+ configurator.ConfigurationBuilder.Conventions.Add(_ => concreteConfigurator);
+
+ return configurator;
+ }}
+ }}
+
+ public interface IDomainModelConfigurator
+ {{
+ ModelConfigurationBuilder ConfigurationBuilder {{ get; }}
+ }}
+
+ file sealed record class DomainModelConfigurator(
+ ModelConfigurationBuilder ConfigurationBuilder)
+ : IDomainModelConfigurator;
+
+ file sealed record class EntityFrameworkIdentityConfigurator(ModelConfigurationBuilder ConfigurationBuilder)
+ : {Constants.DomainModelingNamespace}.Configuration.IIdentityConfigurator
+ {{
+ public void ConfigureIdentity<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TIdentity, TUnderlying>(
+ in {Constants.DomainModelingNamespace}.Configuration.IIdentityConfigurator.Args _)
+ where TIdentity : IIdentity, ISerializableDomainObject
+ where TUnderlying : notnull, IEquatable, IComparable
+ {{
+ // Configure properties of the type
+ this.ConfigurationBuilder.Properties()
+ .HaveConversion>();
+
+ // Configure non-property occurrences of the type, such as in CAST(), SUM(), AVG(), etc.
+ this.ConfigurationBuilder.DefaultTypeMapping()
+ .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))
+ this.ConfigurationBuilder.DefaultTypeMapping()
+ .HasPrecision(28, 0);
+ }}
+
+ private sealed class IdentityValueObjectConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TModel, TProvider>
+ : ValueConverter
+ where TModel : ISerializableDomainObject
+ {{
+ public IdentityValueObjectConverter()
+ : base(
+ DomainObjectSerializer.CreateSerializeExpression(),
+ DomainObjectSerializer.CreateDeserializeExpression(),
+ new ConverterMappingHints(precision: 28, scale: 0)) // For decimal IDs
+ {{
+ }}
+ }}
+ }}
+
+ file sealed record class EntityFrameworkWrapperValueObjectConfigurator(
+ ModelConfigurationBuilder ConfigurationBuilder)
+ : {Constants.DomainModelingNamespace}.Configuration.IWrapperValueObjectConfigurator
+ {{
+ public void ConfigureWrapperValueObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TWrapper, TValue>(
+ in {Constants.DomainModelingNamespace}.Configuration.IWrapperValueObjectConfigurator.Args _)
+ where TWrapper : IWrapperValueObject, ISerializableDomainObject
+ where TValue : notnull
+ {{
+ // Configure properties of the type
+ this.ConfigurationBuilder.Properties()
+ .HaveConversion>();
+
+ // Configure non-property occurrences of the type, such as in CAST(), SUM(), AVG(), etc.
+ this.ConfigurationBuilder.DefaultTypeMapping()
+ .HasConversion>();
+ }}
+
+ private sealed class WrapperValueObjectConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TModel, TProvider>
+ : ValueConverter
+ where TModel : ISerializableDomainObject
+ {{
+ public WrapperValueObjectConverter()
+ : base(
+ DomainObjectSerializer.CreateSerializeExpression(),
+ DomainObjectSerializer.CreateDeserializeExpression())
+ {{
+ }}
+ }}
+ }}
+
+ file sealed record class EntityFrameworkEntityConfigurator(
+ Action InvokeConfigurationCallbacks)
+ : {Constants.DomainModelingNamespace}.Configuration.IEntityConfigurator, {Constants.DomainModelingNamespace}.Configuration.IDomainEventConfigurator, IEntityTypeAddedConvention, IModelFinalizingConvention
+ {{
+ private Dictionary EntityTypeConventionsByType {{ get; }} = new Dictionary();
+
+ public void ProcessEntityTypeAdded(IConventionEntityTypeBuilder entityTypeBuilder, IConventionContext context)
+ {{
+ var type = entityTypeBuilder.Metadata.ClrType;
+ if (!type.IsAbstract && !type.IsInterface && !type.IsGenericTypeDefinition)
+ this.EntityTypeConventionsByType[entityTypeBuilder.Metadata.ClrType] = entityTypeBuilder.Metadata;
+ }}
+
+ public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context)
+ {{
+ this.InvokeConfigurationCallbacks();
+ }}
+
+ public void ConfigureEntity<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TEntity>(
+ in {Constants.DomainModelingNamespace}.Configuration.IEntityConfigurator.Args args)
+ where TEntity : IEntity
+ {{
+ if (!this.EntityTypeConventionsByType.TryGetValue(typeof(TEntity), out var entityTypeConvention))
+ return;
+
+#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)));
+#pragma warning restore EF1001 // Internal EF Core API usage
+ }}
+
+ public void ConfigureDomainEvent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TDomainEvent>(
+ in {Constants.DomainModelingNamespace}.Configuration.IDomainEventConfigurator.Args args)
+ where TDomainEvent : IDomainObject
+ {{
+ if (!this.EntityTypeConventionsByType.TryGetValue(typeof(TDomainEvent), out var entityTypeConvention))
+ return;
+
+#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)));
+#pragma warning restore EF1001 // Internal EF Core API usage
+ }}
+
+ private sealed class UninitializedInstantiationBinding
+ : InstantiationBinding
+ {{
+ private static readonly MethodInfo GetUninitializedObjectMethod = typeof(RuntimeHelpers).GetMethod(nameof(RuntimeHelpers.GetUninitializedObject))!;
+
+ public override Type RuntimeType {{ get; }}
+ private Expression? Expression {{ get; }}
+
+ public UninitializedInstantiationBinding(
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type runtimeType,
+ Expression? expression = null)
+ : base(Array.Empty())
+ {{
+ this.RuntimeType = runtimeType;
+ this.Expression = expression;
+ }}
+
+ public override Expression CreateConstructorExpression(ParameterBindingInfo bindingInfo)
+ {{
+ return this.Expression ??
+ Expression.Convert(
+ Expression.Call(method: GetUninitializedObjectMethod, arguments: Expression.Constant(this.RuntimeType)),
+ this.RuntimeType);
+ }}
+
+ public override InstantiationBinding With(IReadOnlyList parameterBindings)
+ {{
+ return this;
+ }}
+ }}
+ }}
+}}
+
+#endif
+";
+
+ AddSource(context, source, "EntityFrameworkDomainModelConfigurationExtensions", $"{Constants.DomainModelingNamespace}.EntityFramework");
+ }
+
+ internal sealed record Generatable : IGeneratable
+ {
+ public bool UsesEntityFrameworkConventions { get; set; }
+ ///
+ /// The referenced assemblies that contain a specific, generated type.
+ /// Does not include the target assembly, since types are currently being generated for that.
+ ///
+ public StructuralList, string>? ReferencedAssembliesWithIdentityConfigurator { get; set; }
+ ///
+ /// The referenced assemblies that contain a specific, generated type.
+ /// Does not include the target assembly, since types are currently being generated for that.
+ ///
+ public StructuralList, string>? ReferencedAssembliesWithWrapperValueObjectConfigurator { get; set; }
+ ///
+ /// The referenced assemblies that contain a specific, generated type.
+ /// Does not include the target assembly, since types are currently being generated for that.
+ ///
+ public StructuralList, string>? ReferencedAssembliesWithEntityConfigurator { get; set; }
+ ///
+ /// The referenced assemblies that contain a specific, generated type.
+ /// Does not include the target assembly, since types are currently being generated for that.
+ ///
+ public StructuralList, string>? ReferencedAssembliesWithDomainEventConfigurator { get; set; }
+ }
+}
diff --git a/DomainModeling.Generator/Constants.cs b/DomainModeling.Generator/Constants.cs
index 921a208..2e90555 100644
--- a/DomainModeling.Generator/Constants.cs
+++ b/DomainModeling.Generator/Constants.cs
@@ -3,12 +3,16 @@ namespace Architect.DomainModeling.Generator;
internal static class Constants
{
public const string DomainModelingNamespace = "Architect.DomainModeling";
- public const string SourceGeneratedAttributeName = "SourceGeneratedAttribute";
- public const string SourceGeneratedAttributeShortName = "SourceGenerated";
+ public const string DomainObjectInterfaceName = "IDomainObject";
public const string ValueObjectInterfaceTypeName = "IValueObject";
public const string ValueObjectTypeName = "ValueObject";
+ public const string WrapperValueObjectInterfaceTypeName = "IWrapperValueObject";
public const string WrapperValueObjectTypeName = "WrapperValueObject";
public const string IdentityInterfaceTypeName = "IIdentity";
public const string EntityTypeName = "Entity";
+ public const string EntityInterfaceName = "IEntity";
public const string DummyBuilderTypeName = "DummyBuilder";
+ public const string SerializableDomainObjectInterfaceTypeName = "ISerializableDomainObject";
+ public const string SerializeDomainObjectMethodName = "Serialize";
+ public const string DeserializeDomainObjectMethodName = "Deserialize";
}
diff --git a/DomainModeling.Generator/DomainEventGenerator.cs b/DomainModeling.Generator/DomainEventGenerator.cs
new file mode 100644
index 0000000..11ecd08
--- /dev/null
+++ b/DomainModeling.Generator/DomainEventGenerator.cs
@@ -0,0 +1,112 @@
+using Architect.DomainModeling.Generator.Common;
+using Architect.DomainModeling.Generator.Configurators;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Architect.DomainModeling.Generator;
+
+[Generator]
+public class DomainEventGenerator : SourceGenerator
+{
+ public override void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var provider = context.SyntaxProvider.CreateSyntaxProvider(FilterSyntaxNode, TransformSyntaxNode)
+ .Where(generatable => generatable is not null)
+ .DeduplicatePartials();
+
+ context.RegisterSourceOutput(provider, GenerateSource!);
+
+ var aggregatedProvider = provider
+ .Collect()
+ .Combine(EntityFrameworkConfigurationGenerator.CreateMetadataProvider(context));
+
+ context.RegisterSourceOutput(aggregatedProvider, DomainModelConfiguratorGenerator.GenerateSourceForDomainEvents!);
+ }
+
+ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancellationToken = default)
+ {
+ // Class or record class
+ if (node is TypeDeclarationSyntax tds && tds is ClassDeclarationSyntax or RecordDeclarationSyntax { ClassOrStructKeyword.ValueText: "class" })
+ {
+ // With relevant attribute
+ if (tds.HasAttributeWithPrefix("DomainEvent"))
+ return true;
+ }
+
+ return false;
+ }
+
+ private static Generatable? TransformSyntaxNode(GeneratorSyntaxContext context, CancellationToken cancellationToken = default)
+ {
+ var model = context.SemanticModel;
+ var tds = (TypeDeclarationSyntax)context.Node;
+ var type = model.GetDeclaredSymbol(tds);
+
+ if (type is null)
+ return null;
+
+ // Only with the attribute
+ if (type.GetAttribute("DomainEventAttribute", Constants.DomainModelingNamespace, arity: 0) is null)
+ return null;
+
+ // Only concrete
+ if (type.IsAbstract)
+ return null;
+
+ // Only non-generic
+ if (type.IsGenericType)
+ return null;
+
+ // Only non-nested
+ if (type.IsNested())
+ return null;
+
+ var result = new Generatable()
+ {
+ TypeLocation = type.Locations.FirstOrDefault(),
+ IsDomainObject = type.IsOrImplementsInterface(type => type.IsType(Constants.DomainObjectInterfaceName, Constants.DomainModelingNamespace, arity: 0), out _),
+ TypeName = type.Name, // Non-generic by filter
+ ContainingNamespace = type.ContainingNamespace.ToString(),
+ };
+
+ var existingComponents = DomainEventTypeComponents.None;
+
+ existingComponents |= DomainEventTypeComponents.DefaultConstructor.If(type.Constructors.Any(ctor =>
+ !ctor.IsStatic && ctor.Parameters.Length == 0 /*&& ctor.DeclaringSyntaxReferences.Length > 0*/));
+
+ result.ExistingComponents = existingComponents;
+
+ return result;
+ }
+
+ private static void GenerateSource(SourceProductionContext context, Generatable generatable)
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+
+ // Require the expected inheritance
+ if (!generatable.IsDomainObject)
+ {
+ context.ReportDiagnostic("DomainEventGeneratorUnexpectedInheritance", "Unexpected inheritance",
+ "Type marked as domain event lacks IDomainObject interface.", DiagnosticSeverity.Warning, generatable.TypeLocation);
+ return;
+ }
+ }
+
+ [Flags]
+ internal enum DomainEventTypeComponents : ulong
+ {
+ None = 0,
+
+ DefaultConstructor = 1 << 1,
+ }
+
+ internal sealed record Generatable : IGeneratable
+ {
+ public bool IsDomainObject { get; set; }
+ public string TypeName { get; set; } = null!;
+ public string ContainingNamespace { get; set; } = null!;
+ public DomainEventTypeComponents ExistingComponents { get; set; }
+ public SimpleLocation? TypeLocation { get; set; }
+ }
+}
diff --git a/DomainModeling.Generator/DomainModeling.Generator.csproj b/DomainModeling.Generator/DomainModeling.Generator.csproj
index 0d6bea0..612a86e 100644
--- a/DomainModeling.Generator/DomainModeling.Generator.csproj
+++ b/DomainModeling.Generator/DomainModeling.Generator.csproj
@@ -6,7 +6,7 @@
Architect.DomainModeling.Generator
Enable
Enable
- 11
+ 12
False
True
True
@@ -17,12 +17,16 @@
IDE0057
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
diff --git a/DomainModeling.Generator/DummyBuilderGenerator.cs b/DomainModeling.Generator/DummyBuilderGenerator.cs
index a9c5d3f..55768a0 100644
--- a/DomainModeling.Generator/DummyBuilderGenerator.cs
+++ b/DomainModeling.Generator/DummyBuilderGenerator.cs
@@ -16,19 +16,16 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex
.DeduplicatePartials()
.Collect();
- context.RegisterSourceOutput(provider, GenerateSource!);
+ context.RegisterSourceOutput(provider.Combine(context.CompilationProvider), GenerateSource!);
}
private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancellationToken = default)
{
- // Subclass
- if (node is not ClassDeclarationSyntax cds || cds.BaseList is null)
- return false;
-
- // Consider any type with SOME 2-param generic "DummyBuilder" inheritance/implementation
- foreach (var baseType in cds.BaseList.Types)
+ // Struct or class or record
+ if (node is TypeDeclarationSyntax tds && tds is StructDeclarationSyntax or ClassDeclarationSyntax or RecordDeclarationSyntax)
{
- if (baseType.Type.HasArityAndName(2, Constants.DummyBuilderTypeName))
+ // With relevant attribute
+ if (tds.HasAttributeWithPrefix("DummyBuilder"))
return true;
}
@@ -37,134 +34,158 @@ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancella
private static Builder? TransformSyntaxNode(GeneratorSyntaxContext context, CancellationToken cancellationToken = default)
{
- cancellationToken.ThrowIfCancellationRequested();
-
var model = context.SemanticModel;
- var cds = (ClassDeclarationSyntax)context.Node;
+ var tds = (TypeDeclarationSyntax)context.Node;
var type = model.GetDeclaredSymbol((TypeDeclarationSyntax)context.Node);
if (type is null)
return null;
- var result = new Builder();
- result.SetAssociatedData(type);
+ // Only with the attribute
+ if (type.GetAttribute("DummyBuilderAttribute", Constants.DomainModelingNamespace, arity: 1) is not AttributeData { AttributeClass: not null } attribute)
+ return null;
- var isManuallyImplementedBuilder = !cds.Modifiers.Any(SyntaxKind.PartialKeyword);
+ var modelType = attribute.AttributeClass.TypeArguments[0];
- if (isManuallyImplementedBuilder) // Do not generate source, but be aware of existence, for potential invocation from newly generated builders
+ var result = new Builder()
+ {
+ TypeFullyQualifiedName = type.ToString(),
+ ModelTypeFullyQualifiedName = modelType.ToString(),
+ IsPartial = tds.Modifiers.Any(SyntaxKind.PartialKeyword),
+ IsRecord = type.IsRecord,
+ IsClass = type.TypeKind == TypeKind.Class,
+ IsAbstract = type.IsAbstract,
+ IsGeneric = type.IsGenericType,
+ IsNested = type.IsNested(),
+ };
+
+ // Manually implemented
+ if (!result.IsPartial) // Do not generate source, but be aware of existence, for potential invocation from newly generated builders
{
- // Only with the intended inheritance
- if (type.BaseType?.IsType(Constants.DummyBuilderTypeName, Constants.DomainModelingNamespace) != true)
- return null;
// Only if non-abstract
if (type.IsAbstract)
return null;
+
// Only if non-generic
if (type.IsGenericType)
return null;
- result.TypeFullyQualifiedName = type.ToString();
- result.IsManuallyImplemented = true;
+ return result;
}
- else // Prepare to generate source
- {
- // Only with the attribute
- if (!type.HasAttribute(Constants.SourceGeneratedAttributeName, Constants.DomainModelingNamespace))
- return null;
-
- // Only with a usable model type
- if (type.BaseType!.TypeArguments[0] is not INamedTypeSymbol modelType)
- return null;
- result.TypeFullyQualifiedName = type.ToString();
- result.IsDummyBuilder = type.BaseType?.IsType(Constants.DummyBuilderTypeName, Constants.DomainModelingNamespace) == true;
- result.IsAbstract = type.IsAbstract;
- result.IsGeneric = type.IsGenericType;
- result.IsNested = type.IsNested();
+ var members = type.GetMembers();
- var members = type.GetMembers();
-
- result.HasBuildMethod = members.Any(member => member.Name == "Build" && member is IMethodSymbol method && method.Parameters.Length == 0);
-
- var suitableCtor = GetSuitableConstructor(modelType);
-
- result.HasSuitableConstructor = suitableCtor is not null;
- result.Checksum = Convert.ToBase64String(context.Node.GetText().GetChecksum().ToArray()); // Many kinds of changes in the file may warrant changes in the generated source, so rely on the source's checksum
- }
+ result.HasSuitableConstructor = GetSuitableConstructor(modelType) is not null;
+ result.HasBuildMethod = members.Any(member => member.Name == "Build" && member is IMethodSymbol method && method.Parameters.Length == 0);
+ result.Checksum = Convert.ToBase64String([.. context.Node.GetText().GetChecksum()]); // Many kinds of changes in the file may warrant changes in the generated source, so rely on the source's checksum
return result;
}
- private static void GenerateSource(SourceProductionContext context, ImmutableArray builders)
+ private static void GenerateSource(SourceProductionContext context, (ImmutableArray Builders, Compilation Compilation) input)
{
context.CancellationToken.ThrowIfCancellationRequested();
- var buildersWithSourceGeneration = builders.Where(builder => !builder.IsManuallyImplemented).ToList();
+ var builders = input.Builders.ToList();
+ var compilation = input.Compilation;
+
var concreteBuilderTypesByModel = builders
.Where(builder => !builder.IsAbstract && !builder.IsGeneric) // Concrete only
- .Where(builder => builder.IsManuallyImplemented || builder.IsDummyBuilder) // Manually implemented or with the correct inheritance for generation only
- .GroupBy(builder => builder.ModelType(), SymbolEqualityComparer.Default) // Deduplicate
- .ToDictionary, ITypeSymbol, INamedTypeSymbol>(group => group.Key, group => group.First().TypeSymbol(), SymbolEqualityComparer.Default);
+ .GroupBy(builder => builder.ModelTypeFullyQualifiedName) // Deduplicate
+ .Select(group => new KeyValuePair(compilation.GetTypeByMetadataName(group.Key), group.First().TypeFullyQualifiedName))
+ .Where(pair => pair.Key is not null)
+ .ToDictionary, ITypeSymbol, string>(pair => pair.Key!, pair => pair.Value, SymbolEqualityComparer.Default);
// Remove models for which multiple builders exist
{
- var buildersWithDuplicateModel = buildersWithSourceGeneration
- .GroupBy(builder => builder.ModelType(), SymbolEqualityComparer.Default)
- .Where(group => group.Count() > 1);
+ var buildersWithDuplicateModel = builders
+ .GroupBy(builder => builder.ModelTypeFullyQualifiedName)
+ .Where(group => group.Count() > 1)
+ .ToList();
// Remove models for which multiple builders exist
foreach (var group in buildersWithDuplicateModel)
{
foreach (var type in group)
- buildersWithSourceGeneration.Remove(type);
+ builders.Remove(type);
context.ReportDiagnostic("DummyBuilderGeneratorDuplicateBuilders", "Duplicate builders",
- $"Multiple dummy builders exist for {group.Key.Name}. Source generation for these builders was skipped.", DiagnosticSeverity.Warning, group.Last().TypeSymbol());
+ $"Multiple dummy builders exist for {group.Key}. Source generation for these builders was skipped.", DiagnosticSeverity.Warning, compilation.GetTypeByMetadataName(group.Last().TypeFullyQualifiedName));
}
}
- foreach (var builder in buildersWithSourceGeneration)
+ foreach (var builder in builders)
{
context.CancellationToken.ThrowIfCancellationRequested();
- var type = builder.TypeSymbol();
- var modelType = builder.ModelType();
+ var type = compilation.GetTypeByMetadataName(builder.TypeFullyQualifiedName);
+ var modelType = type?.GetAttribute("DummyBuilderAttribute", Constants.DomainModelingNamespace, arity: 1) is AttributeData { AttributeClass: not null } attribute
+ ? attribute.AttributeClass.TypeArguments[0]
+ : null;
- // Only with a suitable constructor
- if (!builder.HasSuitableConstructor)
+ // No source generation, only above analyzers
+ if (!builder.IsPartial)
{
- context.ReportDiagnostic("DummyBuilderGeneratorNoSuitableConstructor", "No suitable constructor",
- $"{type.Name} could not find a suitable constructor on {modelType.Name}.", DiagnosticSeverity.Warning, type);
+ context.ReportDiagnostic("DummyBuilderGeneratorNonPartialType", "Non-partial dummy builder type",
+ "Type marked as dummy builder is not marked as 'partial'. To get source generation, add the 'partial' keyword.", DiagnosticSeverity.Info, type);
+ continue;
+ }
+
+ // Require being able to find the builder type
+ if (type is null)
+ {
+ context.ReportDiagnostic("DummyBuilderGeneratorUnexpectedType", "Unexpected type",
+ $"Type marked as dummy builder has unexpected type '{builder.TypeFullyQualifiedName}'.", DiagnosticSeverity.Warning, type);
+ continue;
}
- // Only with the intended inheritance
- if (!builder.IsDummyBuilder)
+
+ // Require being able to find the model type
+ if (modelType is null)
{
- context.ReportDiagnostic("DummyBuilderGeneratorUnexpectedInheritance", "Unexpected base class",
- "The type marked as source-generated has an unexpected base class. Did you mean DummyBuilder?", DiagnosticSeverity.Warning, type);
+ context.ReportDiagnostic("DummyBuilderGeneratorUnexpectedModelType", "Unexpected model type",
+ $"Type marked as dummy builder has unexpected model type '{builder.ModelTypeFullyQualifiedName}'.", DiagnosticSeverity.Warning, type);
continue;
}
+
+ // Only if class
+ if (!builder.IsClass)
+ {
+ context.ReportDiagnostic("DummyBuilderGeneratorValueType", "Source-generated struct 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, type);
+ continue;
+ }
+
// Only if non-abstract
if (builder.IsAbstract)
{
context.ReportDiagnostic("DummyBuilderGeneratorAbstractType", "Source-generated abstract type",
- "The type was not source-generated because it is abstract.", DiagnosticSeverity.Warning, type);
+ "The type was not source-generated because it is abstract. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning, type);
continue;
}
+
// Only if non-generic
if (builder.IsGeneric)
{
context.ReportDiagnostic("DummyBuilderGeneratorGenericType", "Source-generated generic type",
- "The type was not source-generated because it is generic.", DiagnosticSeverity.Warning, type);
+ "The type was not source-generated because it is generic. To disable source generation, remove the 'partial' keyword.", DiagnosticSeverity.Warning, type);
continue;
}
+
// Only if non-nested
if (builder.IsNested)
{
context.ReportDiagnostic("DummyBuilderGeneratorNestedType", "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.", DiagnosticSeverity.Warning, 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, type);
continue;
}
+ // Only with a suitable constructor
+ if (!builder.HasSuitableConstructor)
+ {
+ context.ReportDiagnostic("DummyBuilderGeneratorNoSuitableConstructor", "No suitable constructor",
+ $"{type.Name} could not find a suitable constructor on {modelType.Name}.", DiagnosticSeverity.Warning, type);
+ }
+
var typeName = type.Name; // Non-generic
var containingNamespace = type.ContainingNamespace.ToString();
var membersByName = type.GetMembers().ToLookup(member => member.Name, StringComparer.OrdinalIgnoreCase);
@@ -174,7 +195,7 @@ private static void GenerateSource(SourceProductionContext context, ImmutableArr
var suitableCtor = GetSuitableConstructor(modelType);
if (suitableCtor is null)
- return;
+ continue;
var ctorParams = suitableCtor.Parameters;
@@ -197,7 +218,7 @@ private static void GenerateSource(SourceProductionContext context, ImmutableArr
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, type);
+ concreteBuilderTypesByModel.Add(modelType, builder.TypeFullyQualifiedName);
}
if (membersByName[$"With{memberName}"].Any(member => member is IMethodSymbol method && method.Parameters.Length == 1 && method.Parameters[0].Type.Equals(param.Type, SymbolEqualityComparer.Default)))
@@ -273,12 +294,18 @@ namespace {containingNamespace}
/// That way, if the constructor changes, only the builder needs to be adjusted, rather than lots of test methods.
///
///
- /* Generated */ public partial class {typeName}
+ /* Generated */ {type.DeclaredAccessibility.ToCodeString()} partial{(builder.IsRecord ? " record" : "")} class {typeName}
{{
{joinedComponents}
+ private {typeName} With(Action<{typeName}> assignment)
+ {{
+ assignment(this);
+ return this;
+ }}
+
{(hasBuildMethod ? "/*" : "")}
- public override {modelType} Build()
+ public {modelType} Build()
{{
var result = new {modelType}(
{modelCtorParams});
@@ -293,9 +320,12 @@ namespace {containingNamespace}
}
}
- private static IMethodSymbol? GetSuitableConstructor(INamedTypeSymbol modelType)
+ private static IMethodSymbol? GetSuitableConstructor(ITypeSymbol modelType)
{
- var result = modelType.Constructors
+ if (modelType is not INamedTypeSymbol namedTypeSymbol)
+ return null;
+
+ var result = namedTypeSymbol.Constructors
.OrderByDescending(ctor => ctor.DeclaredAccessibility) // Most accessible first
.ThenBy(ctor => ctor.Parameters.Length > 0 ? 0 : 1) // Prefer a non-default ctor
.ThenBy(ctor => ctor.Parameters.Length) // Shortest first (the most basic non-default option)
@@ -307,23 +337,15 @@ namespace {containingNamespace}
private sealed record Builder : IGeneratable
{
public string TypeFullyQualifiedName { get; set; } = null!;
- public bool IsDummyBuilder { get; set; }
+ public string ModelTypeFullyQualifiedName { get; set; } = null!;
+ public bool IsPartial { get; set; }
+ public bool IsRecord { get; set; }
+ public bool IsClass { get; set; }
public bool IsAbstract { get; set; }
public bool IsGeneric { get; set; }
public bool IsNested { get; set; }
- public bool IsManuallyImplemented { get; set; }
public bool HasBuildMethod { get; set; }
public bool HasSuitableConstructor { get; set; }
public string? Checksum { get; set; }
-
- public INamedTypeSymbol TypeSymbol()
- {
- return this.GetAssociatedData();
- }
-
- public INamedTypeSymbol ModelType()
- {
- return (INamedTypeSymbol)this.TypeSymbol().BaseType!.TypeArguments[0];
- }
}
}
diff --git a/DomainModeling.Generator/EntityGenerator.cs b/DomainModeling.Generator/EntityGenerator.cs
new file mode 100644
index 0000000..a677e5f
--- /dev/null
+++ b/DomainModeling.Generator/EntityGenerator.cs
@@ -0,0 +1,112 @@
+using Architect.DomainModeling.Generator.Common;
+using Architect.DomainModeling.Generator.Configurators;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Architect.DomainModeling.Generator;
+
+[Generator]
+public class EntityGenerator : SourceGenerator
+{
+ public override void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var provider = context.SyntaxProvider.CreateSyntaxProvider(FilterSyntaxNode, TransformSyntaxNode)
+ .Where(generatable => generatable is not null)
+ .DeduplicatePartials();
+
+ context.RegisterSourceOutput(provider, GenerateSource!);
+
+ var aggregatedProvider = provider
+ .Collect()
+ .Combine(EntityFrameworkConfigurationGenerator.CreateMetadataProvider(context));
+
+ context.RegisterSourceOutput(aggregatedProvider, DomainModelConfiguratorGenerator.GenerateSourceForEntities!);
+ }
+
+ private static bool FilterSyntaxNode(SyntaxNode node, CancellationToken cancellationToken = default)
+ {
+ // Class or record class
+ if (node is TypeDeclarationSyntax tds && tds is ClassDeclarationSyntax or RecordDeclarationSyntax { ClassOrStructKeyword.ValueText: "class" })
+ {
+ // With relevant attribute
+ if (tds.HasAttributeWithPrefix("Entity"))
+ return true;
+ }
+
+ return false;
+ }
+
+ private static Generatable? TransformSyntaxNode(GeneratorSyntaxContext context, CancellationToken cancellationToken = default)
+ {
+ var model = context.SemanticModel;
+ var tds = (TypeDeclarationSyntax)context.Node;
+ var type = model.GetDeclaredSymbol(tds);
+
+ if (type is null)
+ return null;
+
+ // Only with the attribute
+ if (type.GetAttribute("EntityAttribute", Constants.DomainModelingNamespace, arity: 0) is null)
+ return null;
+
+ // Only concrete
+ if (type.IsAbstract)
+ return null;
+
+ // Only non-generic
+ if (type.IsGenericType)
+ return null;
+
+ // Only non-nested
+ if (type.IsNested())
+ return null;
+
+ var result = new Generatable()
+ {
+ TypeLocation = type.Locations.FirstOrDefault(),
+ IsEntity = type.IsOrImplementsInterface(type => type.IsType(Constants.EntityInterfaceName, Constants.DomainModelingNamespace, arity: 0), out _),
+ TypeName = type.Name, // Non-generic by filter
+ ContainingNamespace = type.ContainingNamespace.ToString(),
+ };
+
+ var existingComponents = EntityTypeComponents.None;
+
+ existingComponents |= EntityTypeComponents.DefaultConstructor.If(type.Constructors.Any(ctor =>
+ !ctor.IsStatic && ctor.Parameters.Length == 0 /*&& ctor.DeclaringSyntaxReferences.Length > 0*/));
+
+ result.ExistingComponents = existingComponents;
+
+ return result;
+ }
+
+ private static void GenerateSource(SourceProductionContext context, Generatable generatable)
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+
+ // Require the expected inheritance
+ if (!generatable.IsEntity)
+ {
+ context.ReportDiagnostic("EntityGeneratorUnexpectedInheritance", "Unexpected inheritance",
+ "Type marked as entity lacks IEntity interface.", DiagnosticSeverity.Warning, generatable.TypeLocation);
+ return;
+ }
+ }
+
+ [Flags]
+ internal enum EntityTypeComponents : ulong
+ {
+ None = 0,
+
+ DefaultConstructor = 1 << 1,
+ }
+
+ internal sealed record Generatable : IGeneratable
+ {
+ public bool IsEntity { get; set; }
+ public string TypeName { get; set; } = null!;
+ public string ContainingNamespace { get; set; } = null!;
+ public EntityTypeComponents ExistingComponents { get; set; }
+ public SimpleLocation? TypeLocation { get; set; }
+ }
+}
diff --git a/DomainModeling.Generator/IGeneratable.cs b/DomainModeling.Generator/IGeneratable.cs
index 37b8b15..680de1a 100644
--- a/DomainModeling.Generator/IGeneratable.cs
+++ b/DomainModeling.Generator/IGeneratable.cs
@@ -17,19 +17,28 @@ internal interface IGeneratable
internal static class GeneratableExtensions
{
- public static readonly ConditionalWeakTable