-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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.
- Loading branch information
Showing
68 changed files
with
4,598 additions
and
1,078 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
48 changes: 48 additions & 0 deletions
48
DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.DomainEvents.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Entities.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
49 changes: 49 additions & 0 deletions
49
DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Identities.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.