Skip to content

Commit da57bf5

Browse files
committed
Added serialization to/from deepest underlying type (recursive).
Also cleaned up the way in which lines not being source generated are outcommented. Also improved source generator performance. Also prevented transient issues when reporting warnings on wrapper types.
1 parent 878c0fa commit da57bf5

29 files changed

+1173
-602
lines changed

DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.Identities.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@ public partial class DomainModelConfiguratorGenerator
77
{
88
internal static void GenerateSourceForIdentities(
99
SourceProductionContext context,
10-
(ImmutableArray<ValueWrapperGenerator.BasicGeneratable> Generatables, (bool HasConfigureConventions, string AssemblyName) Metadata) input)
10+
(ImmutableArray<ValueWrapperGenerator.BasicGeneratable> ValueWrappers, (bool HasConfigureConventions, string AssemblyName) Metadata) input)
1111
{
1212
context.CancellationToken.ThrowIfCancellationRequested();
1313

14-
// Generate the method only if we have any generatables, or if we are an assembly in which ConfigureConventions() is called
15-
if (!input.Generatables.Any() && !input.Metadata.HasConfigureConventions)
14+
// Generate the method only if we have any value wrappers, or if we are an assembly in which ConfigureConventions() is called
15+
if (!input.ValueWrappers.Any() && !input.Metadata.HasConfigureConventions)
1616
return;
1717

1818
var targetNamespace = input.Metadata.AssemblyName;
1919

20-
var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.Generatables.Select(generatable => $"""
21-
configurator.ConfigureIdentity<{generatable.ContainingNamespace}.{generatable.TypeName}, {generatable.UnderlyingTypeFullyQualifiedName}>({Environment.NewLine} new Architect.DomainModeling.Configuration.IIdentityConfigurator.Args());
22-
"""));
20+
var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.ValueWrappers
21+
.Where(generatable => generatable.IsIdentity)
22+
.Select(generatable => (Generatable: generatable, CoreTypeName: ValueWrapperGenerator.GetCoreTypeFullyQualifiedName(input.ValueWrappers, generatable.TypeName, generatable.ContainingNamespace)))
23+
.Select(tuple => $$"""
24+
configurator.ConfigureIdentity<{{tuple.Generatable.ContainingNamespace}}.{{tuple.Generatable.TypeName}}, {{tuple.Generatable.UnderlyingTypeFullyQualifiedName}}, {{tuple.CoreTypeName}}>({{Environment.NewLine}} new Architect.DomainModeling.Configuration.IIdentityConfigurator.Args());
25+
"""));
2326

2427
var source = $@"
2528
using Architect.DomainModeling;

DomainModeling.Generator/Configurators/DomainModelConfiguratorGenerator.WrapperValueObjects.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@ public partial class DomainModelConfiguratorGenerator
77
{
88
internal static void GenerateSourceForWrapperValueObjects(
99
SourceProductionContext context,
10-
(ImmutableArray<ValueWrapperGenerator.BasicGeneratable> Generatables, (bool HasConfigureConventions, string AssemblyName) Metadata) input)
10+
(ImmutableArray<ValueWrapperGenerator.BasicGeneratable> ValueWrappers, (bool HasConfigureConventions, string AssemblyName) Metadata) input)
1111
{
1212
context.CancellationToken.ThrowIfCancellationRequested();
1313

14-
// Generate the method only if we have any generatables, or if we are an assembly in which ConfigureConventions() is called
15-
if (!input.Generatables.Any() && !input.Metadata.HasConfigureConventions)
14+
// Generate the method only if we have any value wrappers, or if we are an assembly in which ConfigureConventions() is called
15+
if (!input.ValueWrappers.Any() && !input.Metadata.HasConfigureConventions)
1616
return;
1717

1818
var targetNamespace = input.Metadata.AssemblyName;
1919

20-
var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.Generatables.Select(generatable => $"""
21-
configurator.ConfigureWrapperValueObject<{generatable.ContainingNamespace}.{generatable.TypeName}, {generatable.UnderlyingTypeFullyQualifiedName}>({Environment.NewLine} new Architect.DomainModeling.Configuration.IWrapperValueObjectConfigurator.Args());
22-
"""));
20+
var configurationText = String.Join($"{Environment.NewLine}\t\t\t", input.ValueWrappers
21+
.Where(generatable => !generatable.IsIdentity)
22+
.Select(generatable => (Generatable: generatable, CoreTypeName: ValueWrapperGenerator.GetCoreTypeFullyQualifiedName(input.ValueWrappers, generatable.TypeName, generatable.ContainingNamespace)))
23+
.Select(tuple => $$"""
24+
configurator.ConfigureWrapperValueObject<{{tuple.Generatable.ContainingNamespace}}.{{tuple.Generatable.TypeName}}, {{tuple.Generatable.UnderlyingTypeFullyQualifiedName}}, {{tuple.CoreTypeName}}>({{Environment.NewLine}} new Architect.DomainModeling.Configuration.IWrapperValueObjectConfigurator.Args());
25+
"""));
2326

2427
var source = $@"
2528
using Architect.DomainModeling;

DomainModeling.Generator/Configurators/EntityFrameworkConfigurationGenerator.cs

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -259,34 +259,36 @@ file sealed record class DomainModelConfigurator(
259259
file sealed record class EntityFrameworkIdentityConfigurator(ModelConfigurationBuilder ConfigurationBuilder)
260260
: Architect.DomainModeling.Configuration.IIdentityConfigurator
261261
{{
262-
public void ConfigureIdentity<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TIdentity, TUnderlying>(
263-
in Architect.DomainModeling.Configuration.IIdentityConfigurator.Args _)
264-
where TIdentity : IIdentity<TUnderlying>, ISerializableDomainObject<TIdentity, TUnderlying>
262+
private static readonly ConverterMappingHints DecimalIdConverterMappingHints = new ConverterMappingHints(precision: 28, scale: 0); // For decimal IDs
263+
264+
public void ConfigureIdentity<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TIdentity, TUnderlying, TCore>(
265+
in Architect.DomainModeling.Configuration.IIdentityConfigurator.Args args)
266+
where TIdentity : IIdentity<TUnderlying>, IDirectValueWrapper<TIdentity, TUnderlying>, ICoreValueWrapper<TIdentity, TCore>
265267
where TUnderlying : notnull, IEquatable<TUnderlying>, IComparable<TUnderlying>
266268
{{
267269
// Configure properties of the type
268270
this.ConfigurationBuilder.Properties<TIdentity>()
269-
.HaveConversion<IdentityValueObjectConverter<TIdentity, TUnderlying>>();
271+
.HaveConversion<IdentityValueObjectConverter<TIdentity, TCore>>();
270272
271273
// Configure non-property occurrences of the type, such as in CAST(), SUM(), AVG(), etc.
272274
this.ConfigurationBuilder.DefaultTypeMapping<TIdentity>()
273-
.HasConversion<IdentityValueObjectConverter<TIdentity, TUnderlying>>();
275+
.HasConversion<IdentityValueObjectConverter<TIdentity, TCore>>();
274276
275277
// The converter's mapping hints are currently ignored by DefaultTypeMapping<T>, which is probably a bug: https://github.com/dotnet/efcore/issues/32533
276-
if (typeof(TUnderlying) == typeof(decimal))
278+
if (typeof(TCore) == typeof(decimal))
277279
this.ConfigurationBuilder.DefaultTypeMapping<TIdentity>()
278280
.HasPrecision(28, 0);
279281
}}
280282
281283
private sealed class IdentityValueObjectConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TModel, TProvider>
282284
: ValueConverter<TModel, TProvider>
283-
where TModel : ISerializableDomainObject<TModel, TProvider>
285+
where TModel : IValueWrapper<TModel, TProvider>
284286
{{
285287
public IdentityValueObjectConverter()
286288
: base(
287-
DomainObjectSerializer.CreateSerializeExpression<TModel, TProvider>(),
288-
DomainObjectSerializer.CreateDeserializeExpression<TModel, TProvider>(),
289-
new ConverterMappingHints(precision: 28, scale: 0)) // For decimal IDs
289+
model => DomainObjectSerializer.Serialize<TModel, TProvider>(model)!,
290+
provider => DomainObjectSerializer.Deserialize<TModel, TProvider>(provider)!,
291+
typeof(TProvider) == typeof(decimal) ? DecimalIdConverterMappingHints : null)
290292
{{
291293
}}
292294
}}
@@ -296,28 +298,28 @@ file sealed record class EntityFrameworkWrapperValueObjectConfigurator(
296298
ModelConfigurationBuilder ConfigurationBuilder)
297299
: Architect.DomainModeling.Configuration.IWrapperValueObjectConfigurator
298300
{{
299-
public void ConfigureWrapperValueObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TWrapper, TValue>(
300-
in Architect.DomainModeling.Configuration.IWrapperValueObjectConfigurator.Args _)
301-
where TWrapper : IWrapperValueObject<TValue>, ISerializableDomainObject<TWrapper, TValue>
301+
public void ConfigureWrapperValueObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TWrapper, TValue, TCore>(
302+
in Architect.DomainModeling.Configuration.IWrapperValueObjectConfigurator.Args args)
303+
where TWrapper : IWrapperValueObject<TValue>, IDirectValueWrapper<TWrapper, TValue>, ICoreValueWrapper<TWrapper, TCore>
302304
where TValue : notnull
303305
{{
304306
// Configure properties of the type
305307
this.ConfigurationBuilder.Properties<TWrapper>()
306-
.HaveConversion<WrapperValueObjectConverter<TWrapper, TValue>>();
308+
.HaveConversion<WrapperValueObjectConverter<TWrapper, TCore>>();
307309
308310
// Configure non-property occurrences of the type, such as in CAST(), SUM(), AVG(), etc.
309311
this.ConfigurationBuilder.DefaultTypeMapping<TWrapper>()
310-
.HasConversion<WrapperValueObjectConverter<TWrapper, TValue>>();
312+
.HasConversion<WrapperValueObjectConverter<TWrapper, TCore>>();
311313
}}
312314
313315
private sealed class WrapperValueObjectConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TModel, TProvider>
314316
: ValueConverter<TModel, TProvider>
315-
where TModel : ISerializableDomainObject<TModel, TProvider>
317+
where TModel : IValueWrapper<TModel, TProvider>
316318
{{
317319
public WrapperValueObjectConverter()
318320
: base(
319-
DomainObjectSerializer.CreateSerializeExpression<TModel, TProvider>(),
320-
DomainObjectSerializer.CreateDeserializeExpression<TModel, TProvider>())
321+
model => DomainObjectSerializer.Serialize<TModel, TProvider>(model)!,
322+
provider => DomainObjectSerializer.Deserialize<TModel, TProvider>(provider)!)
321323
{{
322324
}}
323325
}}
@@ -350,7 +352,7 @@ public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConven
350352
351353
#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
352354
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."");
353-
entityType.ConstructorBinding = new UninitializedInstantiationBinding(typeof(TEntity), DomainObjectSerializer.CreateDeserializeExpression(typeof(TEntity)));
355+
entityType.ConstructorBinding = UninitializedInstantiationBinding.Create(() => DomainObjectSerializer.Deserialize<TEntity>());
354356
#pragma warning restore EF1001 // Internal EF Core API usage
355357
}}
356358
@@ -363,7 +365,7 @@ public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConven
363365
364366
#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
365367
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."");
366-
entityType.ConstructorBinding = new UninitializedInstantiationBinding(typeof(TDomainEvent), DomainObjectSerializer.CreateDeserializeExpression(typeof(TDomainEvent)));
368+
entityType.ConstructorBinding = UninitializedInstantiationBinding.Create(() => DomainObjectSerializer.Deserialize<TDomainEvent>());
367369
#pragma warning restore EF1001 // Internal EF Core API usage
368370
}}
369371
@@ -373,23 +375,29 @@ private sealed class UninitializedInstantiationBinding
373375
private static readonly MethodInfo GetUninitializedObjectMethod = typeof(RuntimeHelpers).GetMethod(nameof(RuntimeHelpers.GetUninitializedObject))!;
374376
375377
public override Type RuntimeType {{ get; }}
376-
private Expression? Expression {{ get; }}
378+
private Expression Expression {{ get; }}
379+
380+
public static UninitializedInstantiationBinding Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(
381+
Expression<Func<T>> expression)
382+
{{
383+
return new UninitializedInstantiationBinding(typeof(T), expression.Body);
384+
}}
377385
378386
public UninitializedInstantiationBinding(
379387
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type runtimeType,
380388
Expression? expression = null)
381389
: base(Array.Empty<ParameterBinding>())
382390
{{
383391
this.RuntimeType = runtimeType;
384-
this.Expression = expression;
392+
this.Expression = expression ??
393+
Expression.Convert(
394+
Expression.Call(method: GetUninitializedObjectMethod, arguments: Expression.Constant(this.RuntimeType)),
395+
this.RuntimeType);
385396
}}
386397
387398
public override Expression CreateConstructorExpression(ParameterBindingInfo bindingInfo)
388399
{{
389-
return this.Expression ??
390-
Expression.Convert(
391-
Expression.Call(method: GetUninitializedObjectMethod, arguments: Expression.Constant(this.RuntimeType)),
392-
this.RuntimeType);
400+
return this.Expression;
393401
}}
394402
395403
public override InstantiationBinding With(IReadOnlyList<ParameterBinding> parameterBindings)

DomainModeling.Generator/SourceProductionContextExtensions.cs renamed to DomainModeling.Generator/DiagnosticReportingExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Architect.DomainModeling.Generator;
66
/// <summary>
77
/// Defines extension methods on <see cref="SourceProductionContext"/>.
88
/// </summary>
9-
internal static class SourceProductionContextExtensions
9+
internal static class DiagnosticReportingExtensions
1010
{
1111
/// <summary>
1212
/// Shorthand extension method to report a diagnostic, with less boilerplate code.

DomainModeling.Generator/EnumExtensions.cs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,14 @@ public static string ToCodeString(this Accessibility accessibility, string unspe
5959
/// <code>myEnum |= MyEnum.SomeFlag.If(1 == 2);</code>
6060
/// </para>
6161
/// </summary>
62+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
6263
public static T If<T>(this T enumValue, bool condition)
6364
where T : unmanaged, Enum
6465
{
65-
return condition ? enumValue : default;
66+
// Branch-free implementation
67+
ReadOnlySpan<T> values = stackalloc T[] { default, enumValue, };
68+
var index = Unsafe.As<bool, byte>(ref condition);
69+
return values[index];
6670
}
6771

6872
/// <summary>
@@ -77,15 +81,20 @@ public static T If<T>(this T enumValue, bool condition)
7781
/// <code>myEnum |= MyEnum.SomeFlag.Unless(1 == 2);</code>
7882
/// </para>
7983
/// </summary>
84+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
8085
public static T Unless<T>(this T enumValue, bool condition)
8186
where T : unmanaged, Enum
8287
{
83-
return condition ? default : enumValue;
88+
// Branch-free implementation
89+
ReadOnlySpan<T> values = stackalloc T[] { enumValue, default, };
90+
var index = Unsafe.As<bool, byte>(ref condition);
91+
return values[index];
8492
}
8593

8694
/// <summary>
87-
/// Efficiently returns whether the <paramref name="enumValue"/> has the given <paramref name="flag"/> set.
95+
/// Efficiently returns whether the <paramref name="subject"/> has the given <paramref name="flag"/>(s) set.
8896
/// </summary>
97+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
8998
public static bool HasFlags<T>(this T subject, T flag)
9099
where T : unmanaged, Enum
91100
{
@@ -107,11 +116,8 @@ public static bool HasFlags<T>(this T subject, T flag)
107116
private static ulong GetNumericValue<T>(T enumValue)
108117
where T : unmanaged, Enum
109118
{
110-
Span<ulong> ulongSpan = stackalloc ulong[] { 0UL };
111-
var span = MemoryMarshal.Cast<ulong, T>(ulongSpan);
112-
113-
span[0] = enumValue;
114-
115-
return ulongSpan[0];
119+
var result = 0UL;
120+
Unsafe.WriteUnaligned(ref Unsafe.As<ulong, byte>(ref result), enumValue);
121+
return result;
116122
}
117123
}

0 commit comments

Comments
 (0)