Skip to content

Commit c4aacab

Browse files
authored
Feature/3.0.0 (#22)
* Implemented major version 3.0.0. * Fixed null returns in FormattingHelper methods. * Fixed warnings/messages caused by new .NET and package versions. * Fixed a typo. * Shortened change log to fit in NuGet's maximum. * Fixed generated deserialization constructors for System.Text.Json and Newtonsoft.Json, which ignored them for being private. 3.0.0: - BREAKING: Platform support: Dropped support for .NET 5.0 (EOL). - BREAKING: Marker attributes: [SourceGenerated] attribute is refactored into [Entity], [ValueObject], [WrapperValueObject<TValue>], etc. Obsolete marking helps with migrating. - BREAKING: DummyBuilder base class: The DummyBuilder<TModel, TModelBuilder> base class is deprecated in favor of the new [DummyBuilder<TModel>] attribute. Obsolete marking helps with migrating. - BREAKING: Private ctors: Source-generated ValueObject types now generate a private default ctor with [JsonConstructor], for logic-free deserialization. This may break deserialization if properties lack an init/set. Analyzer included. - BREAKING: Init properties: A new analyzer warns if a WrapperValueObject's Value property lacks an init/set, because logic-free deserialization then requires a workaround. - BREAKING: ISerializableDomainObject interface: Wrapper value objects and identities now require the new ISerializableDomainObject<TModel, TValue> interface (generated automatically). - Feature: Custom inheritance: Source generation with custom base classes is now easy, with marker attributes identifying the concrete types. - Feature: Optional inheritance: For source-generated value objects, wrappers, and identities, the base type or interface is generated and can be omitted. - Feature: DomainObjectSerializer (.NET 7+): The new DomainObjectSerializer type can be used to (de)serialize identities and wrappers without running any domain logic (such as parameterized ctors), and customizable per type. - Feature: Entity Framework mappings (.NET 7+): If Entity Framework is used, mappings by convention (that also bypass ctors) can be generated. Override DbContext.ConfigureConventions() and call ConfigureDomainModelConventions(). Its action param allows all identities, wrapper value objects, entities, and/or domain events to be mapped, even in a trimmer-safe way. - Feature: Miscellaneous mappings: Other third party components can similarly map domain objects. See the readme. - Feature: Marker attributes: Non-partial types with the new marker attributes skip source generation, but can still participate in mappings. - Feature: Record struct identities: Explicitly declared identity types now support "record struct", allowing their curly braces to be omitted: `public partial record struct GeneratedId;` - Feature: ValueObject validation helpers: Added ValueObject.ContainsNonPrintableCharactersOrDoubleQuotes(), a common validation requirement for proper names. - Feature: Formattable and parsable interfaces (.NET 7+): Generated identities and wrappers now implement IFormattable, IParsable<TSelf>, ISpanFormattable, and ISpanParsable<TSelf>, recursing into the wrapped type's implementation. - Feature: UTF-8 formattable and parsable interfaces (.NET 8+): Generated identities and wrappers now implement IUtf8SpanFormattable and IUtf8SpanParsable<TSelf>, recursing into the wrapped type's implementation. - Enhancement: JSON converters (.NET 7+): All generated JSON converters now pass through the new Serialize() and Deserialize() methods, for customizable and logic-free (de)serialization. - Enhancement: JSON converters (.NET 7+): ReadAsPropertyName() and WriteAsPropertyName() in generated JSON converters now recurse into the wrapped type's converter and also pass through the new Serialize() and Deserialize() methods. - Bug fix: IDE stability: Fixed a compile-time bug that could cause some of the IDE's features to crash, such as certain analyzers. - Minor feature: Additional interfaces: IEntity and IWrapperValueObject<TValue> interfaces are now available.
1 parent 26f8df1 commit c4aacab

File tree

68 files changed

+4598
-1078
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+4598
-1078
lines changed

DomainModeling.Example/CharacterSet.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ namespace Architect.DomainModeling.Example;
33
/// <summary>
44
/// Demonstrates structural equality with collections.
55
/// </summary>
6-
[SourceGenerated]
7-
public partial class CharacterSet : ValueObject
6+
[ValueObject]
7+
public partial class CharacterSet
88
{
99
public override string ToString() => $"[{String.Join(", ", this.Characters)}]";
1010

11-
public IReadOnlySet<char> Characters { get; }
11+
public IReadOnlySet<char> Characters { get; private init; }
1212

1313
public CharacterSet(IEnumerable<char> characters)
1414
{

DomainModeling.Example/Color.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ namespace Architect.DomainModeling.Example;
22

33
// Use "Go To Definition" on the type to view the source-generated partial
44
// Uncomment the IComparable interface to see how the generated code changes
5-
[SourceGenerated]
6-
public partial class Color : ValueObject//, IComparable<Color>
5+
[ValueObject]
6+
public partial class Color //: IComparable<Color>
77
{
88
public static Color RedColor { get; } = new Color(red: UInt16.MaxValue, green: 0, blue: 0);
99
public static Color GreenColor { get; } = new Color(red: 0, green: UInt16.MaxValue, blue: 0);
1010
public static Color BlueColor { get; } = new Color(red: 0, green: 0, blue: UInt16.MaxValue);
1111

12-
public ushort Red { get; }
13-
public ushort Green { get; }
14-
public ushort Blue { get; }
12+
public ushort Red { get; private init; }
13+
public ushort Green { get; private init; }
14+
public ushort Blue { get; private init; }
1515

1616
public Color(ushort red, ushort green, ushort blue)
1717
{

DomainModeling.Example/Description.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ namespace Architect.DomainModeling.Example;
22

33
// Use "Go To Definition" on the type to view the source-generated partial
44
// Uncomment the IComparable interface to see how the generated code changes
5-
[SourceGenerated]
6-
public partial class Description : WrapperValueObject<string>//, IComparable<Description>
5+
[WrapperValueObject<string>]
6+
public partial class Description //: IComparable<Description>
77
{
88
// For string wrappers, we must define how they are compared
99
protected override StringComparison StringComparison => StringComparison.OrdinalIgnoreCase;
1010

1111
// Any component that we define manually is omitted by the generated code
1212
// For example, we can explicitly define the Value property to have greater clarity, since it is quintessential
13-
public string Value { get; }
13+
public string Value { get; private init; }
1414

1515
// An explicitly defined constructor allows us to enforce the domain rules and invariants
1616
public Description(string value)

DomainModeling.Example/DomainModeling.Example.csproj

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net7.0</TargetFramework>
5+
<TargetFramework>net6.0</TargetFramework>
66
<AssemblyName>Architect.DomainModeling.Example</AssemblyName>
77
<RootNamespace>Architect.DomainModeling.Example</RootNamespace>
88
<Nullable>Enable</Nullable>
99
<ImplicitUsings>Enable</ImplicitUsings>
1010
<IsPackable>False</IsPackable>
1111
<IsTrimmable>True</IsTrimmable>
12+
<LangVersion>12</LangVersion>
13+
</PropertyGroup>
14+
15+
<PropertyGroup>
16+
<!-- IDE0290: Use primary constructor - domain objects tend to have complex ctor logic, and we want to be consistent even when ctors are simple -->
17+
<NoWarn>IDE0290</NoWarn>
1218
</PropertyGroup>
1319

1420
<ItemGroup>

DomainModeling.Example/PaymentDummyBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
namespace Architect.DomainModeling.Example;
22

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace Architect.DomainModeling.Generator;
4+
5+
/// <summary>
6+
/// Provides extension methods that help inspect assemblies.
7+
/// </summary>
8+
public static class AssemblyInspectionExtensions
9+
{
10+
/// <summary>
11+
/// Enumerates the given <see cref="IAssemblySymbol"/> and all of its referenced <see cref="IAssemblySymbol"/> instances, recursively.
12+
/// Does not deduplicate.
13+
/// </summary>
14+
/// <param name="predicate">A predicate that can filter out assemblies and prevent further recursion into them.</param>
15+
public static IEnumerable<IAssemblySymbol> EnumerateAssembliesRecursively(this IAssemblySymbol assemblySymbol, Func<IAssemblySymbol, bool>? predicate = null)
16+
{
17+
if (predicate is not null && !predicate(assemblySymbol))
18+
yield break;
19+
20+
yield return assemblySymbol;
21+
22+
foreach (var module in assemblySymbol.Modules)
23+
foreach (var assembly in module.ReferencedAssemblySymbols)
24+
foreach (var nestedAssembly in EnumerateAssembliesRecursively(assembly, predicate))
25+
yield return nestedAssembly;
26+
}
27+
28+
/// <summary>
29+
/// Enumerates all non-nested types in the given <see cref="INamespaceSymbol"/>, recursively.
30+
/// </summary>
31+
public static IEnumerable<INamedTypeSymbol> EnumerateNonNestedTypes(this INamespaceSymbol namespaceSymbol)
32+
{
33+
foreach (var type in namespaceSymbol.GetTypeMembers())
34+
yield return type;
35+
36+
foreach (var childNamespace in namespaceSymbol.GetNamespaceMembers())
37+
foreach (var type in EnumerateNonNestedTypes(childNamespace))
38+
yield return type;
39+
}
40+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.Text;
3+
4+
namespace Architect.DomainModeling.Generator.Common;
5+
6+
/// <summary>
7+
/// Represents a <see cref="Location"/> as a simple, serializable structure.
8+
/// </summary>
9+
internal sealed record class SimpleLocation
10+
{
11+
public string FilePath { get; }
12+
public TextSpan TextSpan { get; }
13+
public LinePositionSpan LineSpan { get; }
14+
15+
public SimpleLocation(Location location)
16+
{
17+
var lineSpan = location.GetLineSpan();
18+
this.FilePath = lineSpan.Path;
19+
this.TextSpan = location.SourceSpan;
20+
this.LineSpan = lineSpan.Span;
21+
}
22+
23+
public SimpleLocation(string filePath, TextSpan textSpan, LinePositionSpan lineSpan)
24+
{
25+
this.FilePath = filePath;
26+
this.TextSpan = textSpan;
27+
this.LineSpan = lineSpan;
28+
}
29+
30+
#nullable disable
31+
public static implicit operator SimpleLocation(Location location) => location is null ? null : new SimpleLocation(location);
32+
public static implicit operator Location(SimpleLocation location) => location is null ? null : Location.Create(location.FilePath, location.TextSpan, location.LineSpan);
33+
#nullable enable
34+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
namespace Architect.DomainModeling.Generator.Common;
2+
3+
/// <summary>
4+
/// Wraps an <see cref="IReadOnlyList{T}"/> in a wrapper with structural equality using the collection's elements.
5+
/// </summary>
6+
/// <typeparam name="TCollection">The type of the collection to wrap.</typeparam>
7+
/// <typeparam name="TElement">The type of the collection's elements.</typeparam>
8+
internal sealed class StructuralList<TCollection, TElement>(
9+
TCollection value)
10+
: IEquatable<StructuralList<TCollection, TElement>>
11+
where TCollection : IReadOnlyList<TElement>
12+
{
13+
public TCollection Value { get; } = value ?? throw new ArgumentNullException(nameof(value));
14+
15+
public override int GetHashCode() => this.Value is TCollection value && value.Count > 0
16+
? CombineHashCodes(
17+
value.Count,
18+
value[0]?.GetHashCode() ?? 0,
19+
value[value.Count - 1]?.GetHashCode() ?? 0)
20+
: 0;
21+
public override bool Equals(object obj) => obj is StructuralList<TCollection, TElement> other && this.Equals(other);
22+
23+
public bool Equals(StructuralList<TCollection, TElement> other)
24+
{
25+
if (other is null)
26+
return false;
27+
28+
var left = this.Value;
29+
var right = other.Value;
30+
31+
if (right.Count != left.Count)
32+
return false;
33+
34+
for (var i = 0; i < left.Count; i++)
35+
if (left[i] is not TElement leftElement ? right[i] is not null : !leftElement.Equals(right[i]))
36+
return false;
37+
38+
return true;
39+
}
40+
41+
private static int CombineHashCodes(int count, int firstHashCode, int lastHashCode)
42+
{
43+
var countInHighBits = (ulong)count << 16;
44+
45+
// In the upper half, combine the count with the first hash code
46+
// In the lower half, combine the count with the last hash code
47+
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)
48+
combined |= (ulong)lastHashCode ^ countInHighBits;
49+
50+
return combined.GetHashCode();
51+
}
52+
53+
public static implicit operator TCollection(StructuralList<TCollection, TElement> instance) => instance.Value;
54+
public static implicit operator StructuralList<TCollection, TElement>(TCollection value) => new StructuralList<TCollection, TElement>(value);
55+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
4+
namespace Architect.DomainModeling.Generator.Configurators;
5+
6+
public partial class DomainModelConfiguratorGenerator
7+
{
8+
internal static void GenerateSourceForDomainEvents(SourceProductionContext context, (ImmutableArray<DomainEventGenerator.Generatable> Generatables, (bool HasConfigureConventions, string AssemblyName) Metadata) input)
9+
{
10+
context.CancellationToken.ThrowIfCancellationRequested();
11+
12+
// Generate the method only if we have any generatables, or if we are an assembly in which ConfigureConventions() is called
13+
if (!input.Generatables.Any() && !input.Metadata.HasConfigureConventions)
14+
return;
15+
16+
var targetNamespace = input.Metadata.AssemblyName;
17+
18+
var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.Generatables.Select(generatable =>
19+
$"configurator.ConfigureDomainEvent<{generatable.ContainingNamespace}.{generatable.TypeName}>({Environment.NewLine} new {Constants.DomainModelingNamespace}.Configuration.IDomainEventConfigurator.Args() {{ HasDefaultConstructor = {(generatable.ExistingComponents.HasFlag(DomainEventGenerator.DomainEventTypeComponents.DefaultConstructor) ? "true" : "false")} }});"));
20+
21+
var source = $@"
22+
using {Constants.DomainModelingNamespace};
23+
24+
#nullable enable
25+
26+
namespace {targetNamespace}
27+
{{
28+
public static class DomainEventDomainModelConfigurator
29+
{{
30+
/// <summary>
31+
/// <para>
32+
/// Invokes a callback on the given <paramref name=""configurator""/> for each marked domain event type in the current assembly.
33+
/// </para>
34+
/// <para>
35+
/// 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.
36+
/// </para>
37+
/// </summary>
38+
public static void ConfigureDomainEvents({Constants.DomainModelingNamespace}.Configuration.IDomainEventConfigurator configurator)
39+
{{
40+
{configurationText}
41+
}}
42+
}}
43+
}}
44+
";
45+
46+
AddSource(context, source, "DomainEventDomainModelConfigurator", targetNamespace);
47+
}
48+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
4+
namespace Architect.DomainModeling.Generator.Configurators;
5+
6+
public partial class DomainModelConfiguratorGenerator
7+
{
8+
internal static void GenerateSourceForEntities(SourceProductionContext context, (ImmutableArray<EntityGenerator.Generatable> Generatables, (bool HasConfigureConventions, string AssemblyName) Metadata) input)
9+
{
10+
context.CancellationToken.ThrowIfCancellationRequested();
11+
12+
// Generate the method only if we have any generatables, or if we are an assembly in which ConfigureConventions() is called
13+
if (!input.Generatables.Any() && !input.Metadata.HasConfigureConventions)
14+
return;
15+
16+
var targetNamespace = input.Metadata.AssemblyName;
17+
18+
var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.Generatables.Select(generatable =>
19+
$"configurator.ConfigureEntity<{generatable.ContainingNamespace}.{generatable.TypeName}>({Environment.NewLine} new {Constants.DomainModelingNamespace}.Configuration.IEntityConfigurator.Args() {{ HasDefaultConstructor = {(generatable.ExistingComponents.HasFlag(EntityGenerator.EntityTypeComponents.DefaultConstructor) ? "true" : "false")} }});"));
20+
21+
var source = $@"
22+
using {Constants.DomainModelingNamespace};
23+
24+
#nullable enable
25+
26+
namespace {targetNamespace}
27+
{{
28+
public static class EntityDomainModelConfigurator
29+
{{
30+
/// <summary>
31+
/// <para>
32+
/// Invokes a callback on the given <paramref name=""configurator""/> for each marked <see cref=""IEntity""/> type in the current assembly.
33+
/// </para>
34+
/// <para>
35+
/// 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.
36+
/// </para>
37+
/// </summary>
38+
public static void ConfigureEntities({Constants.DomainModelingNamespace}.Configuration.IEntityConfigurator configurator)
39+
{{
40+
{configurationText}
41+
}}
42+
}}
43+
}}
44+
";
45+
46+
AddSource(context, source, "EntityDomainModelConfigurator", targetNamespace);
47+
}
48+
}

0 commit comments

Comments
 (0)