From 660faf96f39d90b93b6548fb4e50df356105aebb Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 9 Feb 2026 14:16:55 -0500 Subject: [PATCH] feat(source-generators): opt-in base class property mapping Add IncludeBaseClassProperties to DynamoMapperAttribute and generator options to include inherited properties for root models, nested inline objects, and dot-notation validation. Update docs and add an inheritance example and generator snapshot tests. Bump package versions. --- Directory.Build.props | 2 +- Directory.Packages.props | 6 +- LayeredCraft.DynamoMapper.slnx | 2 + docs/api-reference/attributes.md | 20 ++ docs/examples/index.md | 3 + docs/examples/inheritance-mapping.md | 41 +++ docs/index.md | 1 + docs/usage/basic-mapping.md | 32 ++ .../DynamoMapper.Inheritance.csproj | 27 ++ examples/DynamoMapper.Inheritance/Program.cs | 52 ++++ mkdocs.yml | 1 + .../Models/ModelClassInfo.cs | 41 ++- .../Options/MapperOptions.cs | 1 + .../NestedObjectTypeAnalyzer.cs | 23 +- .../PropertyMapping/PropertySymbolLookup.cs | 76 +++++ .../DynamoMapperAttribute.cs | 15 + .../InheritanceVerifyTests.cs | 286 ++++++++++++++++++ ...deBaseProperties#OrderMapper.g.verified.cs | 34 +++ ...thOptIn_Succeeds#OrderMapper.g.verified.cs | 36 +++ ...seProperty_WithoutOptIn_Fails.verified.txt | 17 ++ ...noreBaseProperty#OrderMapper.g.verified.cs | 34 +++ ...ldOnBaseProperty#OrderMapper.g.verified.cs | 36 +++ ...BindBaseProperty#OrderMapper.g.verified.cs | 37 +++ ...esBaseProperties#OrderMapper.g.verified.cs | 36 +++ ...wing_DerivedWins#OrderMapper.g.verified.cs | 36 +++ 25 files changed, 867 insertions(+), 28 deletions(-) create mode 100644 docs/examples/inheritance-mapping.md create mode 100644 examples/DynamoMapper.Inheritance/DynamoMapper.Inheritance.csproj create mode 100644 examples/DynamoMapper.Inheritance/Program.cs create mode 100644 src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertySymbolLookup.cs create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/InheritanceVerifyTests.cs create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_Default_DoesNotIncludeBaseProperties#OrderMapper.g.verified.cs create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithoutOptIn_Fails.verified.txt create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_CanIgnoreBaseProperty#OrderMapper.g.verified.cs create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_CanOverrideFieldOnBaseProperty#OrderMapper.g.verified.cs create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_ConstructorParameter_CanBindBaseProperty#OrderMapper.g.verified.cs create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_IncludesBaseProperties#OrderMapper.g.verified.cs create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_Shadowing_DerivedWins#OrderMapper.g.verified.cs diff --git a/Directory.Build.props b/Directory.Build.props index a4e61f2..a99f456 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.0.1 + 1.0.2 MIT diff --git a/Directory.Packages.props b/Directory.Packages.props index 771085c..9a43326 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,13 +9,13 @@ - + - + @@ -26,7 +26,7 @@ - + diff --git a/LayeredCraft.DynamoMapper.slnx b/LayeredCraft.DynamoMapper.slnx index 271d710..623c4a8 100644 --- a/LayeredCraft.DynamoMapper.slnx +++ b/LayeredCraft.DynamoMapper.slnx @@ -31,6 +31,7 @@ + @@ -55,6 +56,7 @@ + diff --git a/docs/api-reference/attributes.md b/docs/api-reference/attributes.md index 691bfc4..a285acc 100644 --- a/docs/api-reference/attributes.md +++ b/docs/api-reference/attributes.md @@ -10,6 +10,7 @@ Marks a static partial class as a mapper and sets defaults. [DynamoMapper( Convention = DynamoNamingConvention.CamelCase, DefaultRequiredness = Requiredness.InferFromNullability, + IncludeBaseClassProperties = false, OmitNullStrings = true, OmitEmptyStrings = false, DateTimeFormat = "O", @@ -21,6 +22,25 @@ public static partial class OrderMapper } ``` +Properties: + +- `Convention` - key naming convention +- `DefaultRequiredness` - default requiredness +- `IncludeBaseClassProperties` - include properties declared on base classes (opt-in) +- `OmitNullStrings` - omit null string attributes +- `OmitEmptyStrings` - omit empty string attributes +- `DateTimeFormat` - `DateTime`/`DateTimeOffset` format +- `TimeSpanFormat` - `TimeSpan` format +- `EnumFormat` - enum format +- `GuidFormat` - `Guid` format + +Notes: + +- When `IncludeBaseClassProperties = true`, inherited properties are included for root models and + nested inline objects. +- If a derived type declares a property with the same name as an inherited property, the derived + property wins. + ## DynamoFieldAttribute Configures mapping for a specific member. Apply multiple times to the mapper class. diff --git a/docs/examples/index.md b/docs/examples/index.md index 21ab3e4..b25bec2 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -9,3 +9,6 @@ These example projects show DynamoMapper features end-to-end. - `examples/DynamoMapper.MapperConstructor` - Constructor/record support and `[DynamoMapperConstructor]` - `examples/DynamoMapper.Nested` - Nested objects and nested collections +- `examples/DynamoMapper.Inheritance` - Base class properties (opt-in) + +See [Inheritance Mapping](inheritance-mapping.md) for details. diff --git a/docs/examples/inheritance-mapping.md b/docs/examples/inheritance-mapping.md new file mode 100644 index 0000000..6c32353 --- /dev/null +++ b/docs/examples/inheritance-mapping.md @@ -0,0 +1,41 @@ +# Inheritance Mapping + +By default, DynamoMapper only maps properties declared on the model type being mapped. It does not +map properties declared on base classes. + +To include inherited properties, enable the mapper option: + +```csharp +using System.Collections.Generic; +using Amazon.DynamoDBv2.Model; +using DynamoMapper.Runtime; + +namespace MyApp.Data; + +[DynamoMapper(IncludeBaseClassProperties = true)] +public static partial class OrderMapper +{ + public static partial Dictionary ToItem(Order source); + + public static partial Order FromItem(Dictionary item); +} + +public class BaseEntity +{ + public string Id { get; set; } = string.Empty; +} + +public class Order : BaseEntity +{ + public string Name { get; set; } = string.Empty; +} +``` + +Behavior notes: + +- When enabled, inherited properties also participate in nested inline mapping. +- If a derived type declares a property with the same name as an inherited property, the derived + property wins. + +See `examples/DynamoMapper.Inheritance` for a complete, runnable example showing both +`IncludeBaseClassProperties = false` (default) and `true`. diff --git a/docs/index.md b/docs/index.md index c995165..61f95b0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -63,6 +63,7 @@ See the [Quick Start Guide](getting-started/quick-start.md) for a complete tutor - **Clean Domain Models** - No attributes required on your domain classes - **Convention-First** - Sensible defaults with selective overrides - **Nested Mapping** - Inline or mapper-based support for nested objects and collections +- **Optional Inheritance Support** - Opt-in mapping for properties declared on base classes - **Single-Table Friendly** - Built-in support for DynamoDB single-table patterns - **Comprehensive Diagnostics** - Clear compile-time errors with actionable messages diff --git a/docs/usage/basic-mapping.md b/docs/usage/basic-mapping.md index 2799c5f..8c3caf3 100644 --- a/docs/usage/basic-mapping.md +++ b/docs/usage/basic-mapping.md @@ -84,6 +84,38 @@ public class Address See `examples/DynamoMapper.Nested` for a complete example. +## Inheritance (Base Class Properties) + +By default, DynamoMapper only considers properties declared on the model type being mapped. It does +not include properties declared on base classes. + +If you want inherited properties to participate in mapping, enable it on the mapper: + +```csharp +[DynamoMapper(IncludeBaseClassProperties = true)] +public static partial class OrderMapper +{ + public static partial Dictionary ToItem(Order source); + public static partial Order FromItem(Dictionary item); +} + +public class BaseEntity +{ + public string Id { get; set; } +} + +public class Order : BaseEntity +{ + public string Name { get; set; } +} +``` + +Notes: + +- This option also affects nested inline object mapping. +- If a derived type declares a property with the same name as an inherited property, the derived + property wins. + ## Constructor Mapping Rules (`FromItem`) Constructor selection is deterministic and follows these priorities. diff --git a/examples/DynamoMapper.Inheritance/DynamoMapper.Inheritance.csproj b/examples/DynamoMapper.Inheritance/DynamoMapper.Inheritance.csproj new file mode 100644 index 0000000..0af4416 --- /dev/null +++ b/examples/DynamoMapper.Inheritance/DynamoMapper.Inheritance.csproj @@ -0,0 +1,27 @@ + + + Exe + net10.0 + 14 + enable + false + + + $(NoWarn);CS1591 + + + + + + + + + diff --git a/examples/DynamoMapper.Inheritance/Program.cs b/examples/DynamoMapper.Inheritance/Program.cs new file mode 100644 index 0000000..6811b08 --- /dev/null +++ b/examples/DynamoMapper.Inheritance/Program.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using Amazon.DynamoDBv2.Model; +using DynamoMapper.Runtime; + +namespace DynamoMapper.Inheritance; + +[DynamoMapper] +internal static partial class OrderMapper_Default +{ + internal static partial Dictionary ToItem(Order source); + + internal static partial Order FromItem(Dictionary item); +} + +[DynamoMapper(IncludeBaseClassProperties = true)] +internal static partial class OrderMapper_WithBaseProps +{ + internal static partial Dictionary ToItem(Order source); + + internal static partial Order FromItem(Dictionary item); +} + +internal class BaseEntity +{ + internal string Id { get; set; } = string.Empty; +} + +internal class Order : BaseEntity +{ + internal string Name { get; set; } = string.Empty; +} + +internal static class Program +{ + private static void Main() + { + var order = new Order { Id = "order-123", Name = "Sample Order" }; + + var itemDefault = OrderMapper_Default.ToItem(order); + var itemWithBase = OrderMapper_WithBaseProps.ToItem(order); + + Console.WriteLine("Default mapper keys: " + string.Join(", ", itemDefault.Keys)); + Console.WriteLine("With base props keys: " + string.Join(", ", itemWithBase.Keys)); + + var roundTripDefault = OrderMapper_Default.FromItem(itemWithBase); + var roundTripWithBase = OrderMapper_WithBaseProps.FromItem(itemWithBase); + + Console.WriteLine("Default mapper Id: '" + roundTripDefault.Id + "'"); + Console.WriteLine("With base props Id: '" + roundTripWithBase.Id + "'"); + } +} diff --git a/mkdocs.yml b/mkdocs.yml index ec6b54c..fe3f088 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -121,6 +121,7 @@ nav: - Single-Table Design: examples/single-table-design.md - Custom Converters: examples/custom-converters.md - Lambda Functions: examples/lambda-functions.md + - Inheritance Mapping: examples/inheritance-mapping.md - Advanced: - Performance: advanced/performance.md - Diagnostics: advanced/diagnostics.md diff --git a/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs b/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs index f4b1980..abf3cb7 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs @@ -26,7 +26,7 @@ GeneratorContext context { context.ThrowIfCancellationRequested(); - var properties = GetModelProperties(modelTypeSymbol); + var properties = GetModelProperties(modelTypeSymbol, context); var varName = GetModelVarName(modelTypeSymbol, fromItemParameterName, context); @@ -71,15 +71,29 @@ GeneratorContext context } } - private static IPropertySymbol[] GetModelProperties(ITypeSymbol modelTypeSymbol) => - modelTypeSymbol - .GetMembers() - .OfType() - .Where(p => IsMappableProperty(p, modelTypeSymbol)) - .ToArray(); + private static IPropertySymbol[] GetModelProperties( + ITypeSymbol modelTypeSymbol, + GeneratorContext context + ) + { + if (modelTypeSymbol is not INamedTypeSymbol namedType) + { + return modelTypeSymbol + .GetMembers() + .OfType() + .Where(p => IsMappableProperty(p, modelTypeSymbol)) + .ToArray(); + } + + return PropertySymbolLookup.GetProperties( + namedType, + context.MapperOptions.IncludeBaseClassProperties, + static (p, declaringType) => IsMappableProperty(p, declaringType) + ); + } - private static bool IsMappableProperty(IPropertySymbol property, ITypeSymbol modelTypeSymbol) => - !property.IsStatic && !(modelTypeSymbol.IsRecord && property.Name == "EqualityContract"); + private static bool IsMappableProperty(IPropertySymbol property, ITypeSymbol declaringType) => + !property.IsStatic && !(declaringType.IsRecord && property.Name == "EqualityContract"); private static string GetModelVarName( ITypeSymbol modelTypeSymbol, @@ -264,10 +278,11 @@ GeneratorContext context var segment = segments[i]; // Find the property on the current type - var property = currentType - .GetMembers() - .OfType() - .FirstOrDefault(p => p.Name == segment); + var property = PropertySymbolLookup.FindPropertyByName( + currentType, + segment, + context.MapperOptions.IncludeBaseClassProperties + ); if (property == null) { diff --git a/src/LayeredCraft.DynamoMapper.Generators/Options/MapperOptions.cs b/src/LayeredCraft.DynamoMapper.Generators/Options/MapperOptions.cs index 3e4de64..bd982a6 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Options/MapperOptions.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Options/MapperOptions.cs @@ -9,6 +9,7 @@ internal class MapperOptions internal string DateTimeFormat { get; set; } = "O"; internal string TimeSpanFormat { get; set; } = "c"; internal Requiredness DefaultRequiredness { get; set; } = Requiredness.InferFromNullability; + internal bool IncludeBaseClassProperties { get; set; } = false; internal string EnumFormat { get; set; } = "G"; internal string GuidFormat { get; set; } = "D"; internal bool OmitEmptyStrings { get; set; } = false; diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/NestedObjectTypeAnalyzer.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/NestedObjectTypeAnalyzer.cs index 0ab5ad1..1e84e52 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/NestedObjectTypeAnalyzer.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/NestedObjectTypeAnalyzer.cs @@ -104,7 +104,7 @@ private static bool IsNestedObjectType(ITypeSymbol type, GeneratorContext contex return false; // Check it has mappable properties - var properties = GetMappableProperties(namedType); + var properties = GetMappableProperties(namedType, context); if (properties.Length == 0) return false; @@ -133,15 +133,16 @@ private static bool IsWellKnownNonNestedType(INamedTypeSymbol type, GeneratorCon /// /// Gets the mappable properties from a type. /// - private static IPropertySymbol[] GetMappableProperties(INamedTypeSymbol type) - { - return type - .GetMembers() - .OfType() - .Where(p => !p.IsStatic && !p.IsIndexer && (p.GetMethod != null || p.SetMethod != null)) - .Where(p => !(type.IsRecord && p.Name == "EqualityContract")) - .ToArray(); - } + private static IPropertySymbol[] GetMappableProperties(INamedTypeSymbol type, GeneratorContext context) => + PropertySymbolLookup.GetProperties( + type, + context.MapperOptions.IncludeBaseClassProperties, + static (p, declaringType) => + !p.IsStatic + && !p.IsIndexer + && (p.GetMethod != null || p.SetMethod != null) + && !(declaringType.IsRecord && p.Name == "EqualityContract") + ); /// /// Analyzes a type for inline code generation, recursively building property specs. @@ -157,7 +158,7 @@ NestedAnalysisContext nestedContext // Add this type to the ancestor chain for cycle detection var contextWithAncestor = nestedContext.WithAncestor(type); - var properties = GetMappableProperties(namedType); + var properties = GetMappableProperties(namedType, nestedContext.Context); var propertySpecs = new List(); var diagnostics = new List(); diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertySymbolLookup.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertySymbolLookup.cs new file mode 100644 index 0000000..c8673eb --- /dev/null +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertySymbolLookup.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace DynamoMapper.Generator.PropertyMapping; + +internal static class PropertySymbolLookup +{ + internal static IPropertySymbol[] GetProperties( + INamedTypeSymbol type, + bool includeBaseTypes, + Func predicate + ) + { + if (!includeBaseTypes) + { + return type + .GetMembers() + .OfType() + .Where(p => predicate(p, type)) + .ToArray(); + } + + var seenNames = new HashSet(StringComparer.Ordinal); + var properties = new List(); + + for (var current = type; current is not null; current = current.BaseType) + { + foreach (var property in current.GetMembers().OfType()) + { + if (!predicate(property, current)) + continue; + + // Derived wins by name - skip base properties with the same name + if (!seenNames.Add(property.Name)) + continue; + + properties.Add(property); + } + } + + return properties.ToArray(); + } + + internal static IPropertySymbol? FindPropertyByName( + ITypeSymbol type, + string name, + bool includeBaseTypes + ) + { + if (type is not INamedTypeSymbol namedType) + return null; + + if (!includeBaseTypes) + { + return namedType + .GetMembers() + .OfType() + .FirstOrDefault(p => p.Name == name); + } + + for (var current = namedType; current is not null; current = current.BaseType) + { + var property = current + .GetMembers() + .OfType() + .FirstOrDefault(p => p.Name == name); + + if (property is not null) + return property; + } + + return null; + } +} diff --git a/src/LayeredCraft.DynamoMapper.Runtime/DynamoMapperAttribute.cs b/src/LayeredCraft.DynamoMapper.Runtime/DynamoMapperAttribute.cs index 5b01267..170ff5f 100644 --- a/src/LayeredCraft.DynamoMapper.Runtime/DynamoMapperAttribute.cs +++ b/src/LayeredCraft.DynamoMapper.Runtime/DynamoMapperAttribute.cs @@ -22,6 +22,21 @@ public sealed class DynamoMapperAttribute : Attribute /// public Requiredness DefaultRequiredness { get; set; } = Requiredness.InferFromNullability; + /// + /// Gets or sets whether properties declared on base classes should be included in mapping. + /// + /// + /// + /// When false (default), only properties declared on the model type are considered. + /// + /// + /// When true, properties declared on base classes are also considered. If a derived + /// type declares a property with the same name as an inherited property, the derived + /// property wins. + /// + /// + public bool IncludeBaseClassProperties { get; set; } = false; + /// Gets or sets whether to omit null string values from the DynamoDB item. /// Default is true. public bool OmitNullStrings { get; set; } = true; diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/InheritanceVerifyTests.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/InheritanceVerifyTests.cs new file mode 100644 index 0000000..dcaf29a --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/InheritanceVerifyTests.cs @@ -0,0 +1,286 @@ +namespace LayeredCraft.DynamoMapper.Generators.Tests; + +public class InheritanceVerifyTests +{ + [Fact] + public async Task Inheritance_Default_DoesNotIncludeBaseProperties() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + internal static partial class OrderMapper + { + internal static partial Dictionary ToItem(Order source); + internal static partial Order FromItem(Dictionary item); + } + + internal class BaseEntity + { + internal string Id { get; set; } = string.Empty; + } + + internal class Order : BaseEntity + { + internal string Name { get; set; } = string.Empty; + } + """, + }, + TestContext.Current.CancellationToken + ); + + [Fact] + public async Task Inheritance_OptIn_IncludesBaseProperties() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper(IncludeBaseClassProperties = true)] + internal static partial class OrderMapper + { + internal static partial Dictionary ToItem(Order source); + internal static partial Order FromItem(Dictionary item); + } + + internal class BaseEntity + { + internal string Id { get; set; } = string.Empty; + } + + internal class Order : BaseEntity + { + internal string Name { get; set; } = string.Empty; + } + """, + }, + TestContext.Current.CancellationToken + ); + + [Fact] + public async Task Inheritance_OptIn_CanIgnoreBaseProperty() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper(IncludeBaseClassProperties = true)] + [DynamoIgnore(nameof(BaseEntity.Id))] + internal static partial class OrderMapper + { + internal static partial Dictionary ToItem(Order source); + internal static partial Order FromItem(Dictionary item); + } + + internal class BaseEntity + { + internal string Id { get; set; } = string.Empty; + } + + internal class Order : BaseEntity + { + internal string Name { get; set; } = string.Empty; + } + """, + }, + TestContext.Current.CancellationToken + ); + + [Fact] + public async Task Inheritance_OptIn_CanOverrideFieldOnBaseProperty() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper(IncludeBaseClassProperties = true)] + [DynamoField(nameof(BaseEntity.Id), AttributeName = "pk")] + internal static partial class OrderMapper + { + internal static partial Dictionary ToItem(Order source); + internal static partial Order FromItem(Dictionary item); + } + + internal class BaseEntity + { + internal string Id { get; set; } = string.Empty; + } + + internal class Order : BaseEntity + { + internal string Name { get; set; } = string.Empty; + } + """, + }, + TestContext.Current.CancellationToken + ); + + [Fact] + public async Task Inheritance_DotNotation_BaseProperty_WithoutOptIn_Fails() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + ExpectedDiagnosticId = "DM0008", + SourceCode = """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + [DynamoField("Address.Line1", AttributeName = "line_1")] + internal static partial class OrderMapper + { + internal static partial Dictionary ToItem(Order source); + internal static partial Order FromItem(Dictionary item); + } + + internal class Order + { + internal Address Address { get; set; } = new(); + } + + internal class AddressBase + { + internal string Line1 { get; set; } = string.Empty; + } + + internal class Address : AddressBase + { + internal string City { get; set; } = string.Empty; + } + """, + }, + TestContext.Current.CancellationToken + ); + + [Fact] + public async Task Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper(IncludeBaseClassProperties = true)] + [DynamoField("Address.Line1", AttributeName = "line_1")] + internal static partial class OrderMapper + { + internal static partial Dictionary ToItem(Order source); + internal static partial Order FromItem(Dictionary item); + } + + internal class Order + { + internal Address Address { get; set; } = new(); + } + + internal class AddressBase + { + internal string Line1 { get; set; } = string.Empty; + } + + internal class Address : AddressBase + { + internal string City { get; set; } = string.Empty; + } + """, + }, + TestContext.Current.CancellationToken + ); + + [Fact] + public async Task Inheritance_OptIn_Shadowing_DerivedWins() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper(IncludeBaseClassProperties = true)] + internal static partial class OrderMapper + { + internal static partial Dictionary ToItem(Order source); + internal static partial Order FromItem(Dictionary item); + } + + internal class BaseEntity + { + internal string Id { get; set; } = string.Empty; + } + + internal class Order : BaseEntity + { + internal new string Id { get; set; } = string.Empty; + internal string Name { get; set; } = string.Empty; + } + """, + }, + TestContext.Current.CancellationToken + ); + + [Fact] + public async Task Inheritance_OptIn_ConstructorParameter_CanBindBaseProperty() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper(IncludeBaseClassProperties = true)] + internal static partial class OrderMapper + { + internal static partial Dictionary ToItem(Order source); + internal static partial Order FromItem(Dictionary item); + } + + internal class BaseEntity + { + internal BaseEntity(string id) => Id = id; + internal string Id { get; } + } + + internal class Order : BaseEntity + { + internal Order(string id, string name) : base(id) => Name = name; + internal string Name { get; } + } + """, + }, + TestContext.Current.CancellationToken + ); +} diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_Default_DoesNotIncludeBaseProperties#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_Default_DoesNotIncludeBaseProperties#OrderMapper.g.verified.cs new file mode 100644 index 0000000..61dfcf9 --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_Default_DoesNotIncludeBaseProperties#OrderMapper.g.verified.cs @@ -0,0 +1,34 @@ +//HintName: OrderMapper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using DynamoMapper.Runtime; +using System.Collections.Generic; +using System.Linq; +using Amazon.DynamoDBv2.Model; + +namespace MyNamespace; + +internal static partial class OrderMapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => + new Dictionary(1) + .SetString("name", source.Name, false, true); + + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) + { + var order = new global::MyNamespace.Order(); + if (item.TryGetString("name", out var var0, Requiredness.InferFromNullability)) order.Name = var0!; + return order; + } +} diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs new file mode 100644 index 0000000..4176c78 --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs @@ -0,0 +1,36 @@ +//HintName: OrderMapper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using DynamoMapper.Runtime; +using System.Collections.Generic; +using System.Linq; +using Amazon.DynamoDBv2.Model; + +namespace MyNamespace; + +internal static partial class OrderMapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => + new Dictionary(1) + .Set("address", new AttributeValue { M = new Dictionary().SetString("city", source.Address.City, false, true).SetString("line_1", source.Address.Line1, false, true) }); + + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) + { + var order = new global::MyNamespace.Order + { + Address = item.TryGetValue("address", out var addressAttr) && addressAttr.M is { } addressMap ? new global::MyNamespace.Address { City = addressMap.GetString("city", Requiredness.Optional), Line1 = addressMap.GetString("line_1", Requiredness.Optional), } : null, + }; + return order; + } +} diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithoutOptIn_Fails.verified.txt b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithoutOptIn_Fails.verified.txt new file mode 100644 index 0000000..40f97c0 --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithoutOptIn_Fails.verified.txt @@ -0,0 +1,17 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (6,0)-(12,1), + Message: The dot-notation path 'Address.Line1' is invalid. Property 'Line1' not found on type 'Address'., + Severity: Error, + Descriptor: { + Id: DM0008, + Title: Invalid dot-notation path, + MessageFormat: The dot-notation path '{0}' is invalid. Property '{1}' not found on type '{2}'., + Category: DynamoMapper.Usage, + DefaultSeverity: Error, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_CanIgnoreBaseProperty#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_CanIgnoreBaseProperty#OrderMapper.g.verified.cs new file mode 100644 index 0000000..61dfcf9 --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_CanIgnoreBaseProperty#OrderMapper.g.verified.cs @@ -0,0 +1,34 @@ +//HintName: OrderMapper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using DynamoMapper.Runtime; +using System.Collections.Generic; +using System.Linq; +using Amazon.DynamoDBv2.Model; + +namespace MyNamespace; + +internal static partial class OrderMapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => + new Dictionary(1) + .SetString("name", source.Name, false, true); + + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) + { + var order = new global::MyNamespace.Order(); + if (item.TryGetString("name", out var var0, Requiredness.InferFromNullability)) order.Name = var0!; + return order; + } +} diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_CanOverrideFieldOnBaseProperty#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_CanOverrideFieldOnBaseProperty#OrderMapper.g.verified.cs new file mode 100644 index 0000000..d5d1261 --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_CanOverrideFieldOnBaseProperty#OrderMapper.g.verified.cs @@ -0,0 +1,36 @@ +//HintName: OrderMapper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using DynamoMapper.Runtime; +using System.Collections.Generic; +using System.Linq; +using Amazon.DynamoDBv2.Model; + +namespace MyNamespace; + +internal static partial class OrderMapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => + new Dictionary(2) + .SetString("name", source.Name, false, true) + .SetString("pk", source.Id, false, true); + + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) + { + var order = new global::MyNamespace.Order(); + if (item.TryGetString("name", out var var0, Requiredness.InferFromNullability)) order.Name = var0!; + if (item.TryGetString("pk", out var var1, Requiredness.InferFromNullability)) order.Id = var1!; + return order; + } +} diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_ConstructorParameter_CanBindBaseProperty#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_ConstructorParameter_CanBindBaseProperty#OrderMapper.g.verified.cs new file mode 100644 index 0000000..832c8c2 --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_ConstructorParameter_CanBindBaseProperty#OrderMapper.g.verified.cs @@ -0,0 +1,37 @@ +//HintName: OrderMapper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using DynamoMapper.Runtime; +using System.Collections.Generic; +using System.Linq; +using Amazon.DynamoDBv2.Model; + +namespace MyNamespace; + +internal static partial class OrderMapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => + new Dictionary(2) + .SetString("name", source.Name, false, true) + .SetString("id", source.Id, false, true); + + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) + { + var order = new global::MyNamespace.Order( + id: item.GetString("id", Requiredness.InferFromNullability), + name: item.GetString("name", Requiredness.InferFromNullability) + ); + return order; + } +} diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_IncludesBaseProperties#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_IncludesBaseProperties#OrderMapper.g.verified.cs new file mode 100644 index 0000000..021a5e6 --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_IncludesBaseProperties#OrderMapper.g.verified.cs @@ -0,0 +1,36 @@ +//HintName: OrderMapper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using DynamoMapper.Runtime; +using System.Collections.Generic; +using System.Linq; +using Amazon.DynamoDBv2.Model; + +namespace MyNamespace; + +internal static partial class OrderMapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => + new Dictionary(2) + .SetString("name", source.Name, false, true) + .SetString("id", source.Id, false, true); + + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) + { + var order = new global::MyNamespace.Order(); + if (item.TryGetString("name", out var var0, Requiredness.InferFromNullability)) order.Name = var0!; + if (item.TryGetString("id", out var var1, Requiredness.InferFromNullability)) order.Id = var1!; + return order; + } +} diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_Shadowing_DerivedWins#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_Shadowing_DerivedWins#OrderMapper.g.verified.cs new file mode 100644 index 0000000..38779f5 --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_OptIn_Shadowing_DerivedWins#OrderMapper.g.verified.cs @@ -0,0 +1,36 @@ +//HintName: OrderMapper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using DynamoMapper.Runtime; +using System.Collections.Generic; +using System.Linq; +using Amazon.DynamoDBv2.Model; + +namespace MyNamespace; + +internal static partial class OrderMapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => + new Dictionary(2) + .SetString("id", source.Id, false, true) + .SetString("name", source.Name, false, true); + + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + internal static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) + { + var order = new global::MyNamespace.Order(); + if (item.TryGetString("id", out var var0, Requiredness.InferFromNullability)) order.Id = var0!; + if (item.TryGetString("name", out var var1, Requiredness.InferFromNullability)) order.Name = var1!; + return order; + } +}