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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions DomainModeling.Example/CharacterSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ namespace Architect.DomainModeling.Example;
/// <summary>
/// Demonstrates structural equality with collections.
/// </summary>
[SourceGenerated]
public partial class CharacterSet : ValueObject
[ValueObject]
public partial class CharacterSet
{
public override string ToString() => $"[{String.Join(", ", this.Characters)}]";

public IReadOnlySet<char> Characters { get; }
public IReadOnlySet<char> Characters { get; private init; }

public CharacterSet(IEnumerable<char> characters)
{
Expand Down
10 changes: 5 additions & 5 deletions DomainModeling.Example/Color.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Color>
[ValueObject]
public partial class Color //: IComparable<Color>
{
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)
{
Expand Down
6 changes: 3 additions & 3 deletions DomainModeling.Example/Description.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>//, IComparable<Description>
[WrapperValueObject<string>]
public partial class Description //: IComparable<Description>
{
// 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)
Expand Down
8 changes: 7 additions & 1 deletion DomainModeling.Example/DomainModeling.Example.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>Architect.DomainModeling.Example</AssemblyName>
<RootNamespace>Architect.DomainModeling.Example</RootNamespace>
<Nullable>Enable</Nullable>
<ImplicitUsings>Enable</ImplicitUsings>
<IsPackable>False</IsPackable>
<IsTrimmable>True</IsTrimmable>
<LangVersion>12</LangVersion>
</PropertyGroup>

<PropertyGroup>
<!-- IDE0290: Use primary constructor - domain objects tend to have complex ctor logic, and we want to be consistent even when ctors are simple -->
<NoWarn>IDE0290</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions DomainModeling.Example/PaymentDummyBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
namespace Architect.DomainModeling.Example;

// The source-generated partial provides an appropriate type summary
[SourceGenerated]
public sealed partial class PaymentDummyBuilder : DummyBuilder<Payment, PaymentDummyBuilder>
[DummyBuilder<Payment>]
public sealed partial class PaymentDummyBuilder
{
// The source-generated partial defines a default value for each property, along with a fluent method to change it

Expand Down
40 changes: 40 additions & 0 deletions DomainModeling.Generator/AssemblyInspectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.CodeAnalysis;

namespace Architect.DomainModeling.Generator;

/// <summary>
/// Provides extension methods that help inspect assemblies.
/// </summary>
public static class AssemblyInspectionExtensions
{
/// <summary>
/// Enumerates the given <see cref="IAssemblySymbol"/> and all of its referenced <see cref="IAssemblySymbol"/> instances, recursively.
/// Does not deduplicate.
/// </summary>
/// <param name="predicate">A predicate that can filter out assemblies and prevent further recursion into them.</param>
public static IEnumerable<IAssemblySymbol> EnumerateAssembliesRecursively(this IAssemblySymbol assemblySymbol, Func<IAssemblySymbol, bool>? 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;
}

/// <summary>
/// Enumerates all non-nested types in the given <see cref="INamespaceSymbol"/>, recursively.
/// </summary>
public static IEnumerable<INamedTypeSymbol> 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;
}
}
34 changes: 34 additions & 0 deletions DomainModeling.Generator/Common/SimpleLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace Architect.DomainModeling.Generator.Common;

/// <summary>
/// Represents a <see cref="Location"/> as a simple, serializable structure.
/// </summary>
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
}
55 changes: 55 additions & 0 deletions DomainModeling.Generator/Common/StructuralList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace Architect.DomainModeling.Generator.Common;

/// <summary>
/// Wraps an <see cref="IReadOnlyList{T}"/> in a wrapper with structural equality using the collection's elements.
/// </summary>
/// <typeparam name="TCollection">The type of the collection to wrap.</typeparam>
/// <typeparam name="TElement">The type of the collection's elements.</typeparam>
internal sealed class StructuralList<TCollection, TElement>(
TCollection value)
: IEquatable<StructuralList<TCollection, TElement>>
where TCollection : IReadOnlyList<TElement>
{
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<TCollection, TElement> other && this.Equals(other);

public bool Equals(StructuralList<TCollection, TElement> 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<TCollection, TElement> instance) => instance.Value;
public static implicit operator StructuralList<TCollection, TElement>(TCollection value) => new StructuralList<TCollection, TElement>(value);
}
Original file line number Diff line number Diff line change
@@ -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<DomainEventGenerator.Generatable> 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
{{
/// <summary>
/// <para>
/// Invokes a callback on the given <paramref name=""configurator""/> for each marked domain event type in the current assembly.
/// </para>
/// <para>
/// 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.
/// </para>
/// </summary>
public static void ConfigureDomainEvents({Constants.DomainModelingNamespace}.Configuration.IDomainEventConfigurator configurator)
{{
{configurationText}
}}
}}
}}
";

AddSource(context, source, "DomainEventDomainModelConfigurator", targetNamespace);
}
}
Original file line number Diff line number Diff line change
@@ -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<EntityGenerator.Generatable> 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
{{
/// <summary>
/// <para>
/// Invokes a callback on the given <paramref name=""configurator""/> for each marked <see cref=""IEntity""/> type in the current assembly.
/// </para>
/// <para>
/// 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.
/// </para>
/// </summary>
public static void ConfigureEntities({Constants.DomainModelingNamespace}.Configuration.IEntityConfigurator configurator)
{{
{configurationText}
}}
}}
}}
";

AddSource(context, source, "EntityDomainModelConfigurator", targetNamespace);
}
}
Original file line number Diff line number Diff line change
@@ -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<IdentityGenerator.Generatable> 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
{{
/// <summary>
/// <para>
/// Invokes a callback on the given <paramref name=""configurator""/> for each marked <see cref=""IIdentity{{T}}""/> type in the current assembly.
/// </para>
/// <para>
/// 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.
/// </para>
/// </summary>
public static void ConfigureIdentities({Constants.DomainModelingNamespace}.Configuration.IIdentityConfigurator configurator)
{{
{configurationText}
}}
}}
}}
";

AddSource(context, source, "IdentityDomainModelConfigurator", targetNamespace);
}
}
Loading