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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>1.0.1</VersionPrefix>
<VersionPrefix>1.0.2</VersionPrefix>
<!-- SPDX license identifier for MIT -->
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<!-- Other useful metadata -->
Expand Down
6 changes: 3 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
<PackageVersion Include="AutoFixture.AutoNSubstitute" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit3" Version="4.19.0" />
<PackageVersion Include="AwesomeAssertions" Version="9.3.0" />
<PackageVersion Include="AWSSDK.DynamoDBv2" Version="4.0.10.9" />
<PackageVersion Include="AWSSDK.DynamoDBv2" Version="4.0.14" />
<PackageVersion Include="Basic.Reference.Assemblies.Net100" Version="1.8.4" />
<PackageVersion Include="Basic.Reference.Assemblies.Net80" Version="1.8.4" />
<PackageVersion Include="Basic.Reference.Assemblies.Net90" Version="1.8.4" />
<PackageVersion Include="Humanizer.Core" Version="3.0.1" />
<PackageVersion Include="LayeredCraft.SourceGeneratorTools" Version="0.1.0-beta.10" />
<PackageVersion Include="Meziantou.Polyfill" Version="1.0.100" />
<PackageVersion Include="Meziantou.Polyfill" Version="1.0.101" />
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" />
Expand All @@ -26,7 +26,7 @@
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="Scriban" Version="6.5.2" />
<PackageVersion Include="Verify.SourceGenerators" Version="2.5.0" />
<PackageVersion Include="Verify.XunitV3" Version="31.9.4" />
<PackageVersion Include="Verify.XunitV3" Version="31.12.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3.mtp-v2" Version="3.2.2" />
</ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions LayeredCraft.DynamoMapper.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
</Folder>
<Folder Name="/docs/examples/">
<File Path="docs\examples\custom-converters.md" />
<File Path="docs\examples\inheritance-mapping.md" />
<File Path="docs\examples\index.md" />
<File Path="docs\examples\lambda-functions.md" />
<File Path="docs\examples\nested-mapping.md" />
Expand All @@ -55,6 +56,7 @@
</Folder>
<Folder Name="/examples/">
<Project Path="examples/DynamoMapper.FieldLevelOverride/DynamoMapper.FieldLevelOverride.csproj" />
<Project Path="examples/DynamoMapper.Inheritance/DynamoMapper.Inheritance.csproj" />
<Project Path="examples/DynamoMapper.MapperConstructor/DynamoMapper.MapperConstructor.csproj" />
<Project Path="examples/DynamoMapper.Nested/DynamoMapper.Nested.csproj" />
<Project Path="examples/DynamoMapper.SimpleExample/DynamoMapper.SimpleExample.csproj" />
Expand Down
20 changes: 20 additions & 0 deletions docs/api-reference/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
41 changes: 41 additions & 0 deletions docs/examples/inheritance-mapping.md
Original file line number Diff line number Diff line change
@@ -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<string, AttributeValue> ToItem(Order source);

public static partial Order FromItem(Dictionary<string, AttributeValue> 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`.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
32 changes: 32 additions & 0 deletions docs/usage/basic-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, AttributeValue> ToItem(Order source);
public static partial Order FromItem(Dictionary<string, AttributeValue> 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.
Expand Down
27 changes: 27 additions & 0 deletions examples/DynamoMapper.Inheritance/DynamoMapper.Inheritance.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.DynamoDBv2" />
</ItemGroup>
<ItemGroup>
<ProjectReference
Include="..\..\src\LayeredCraft.DynamoMapper.Generators\LayeredCraft.DynamoMapper.Generators.csproj"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
/>
<ProjectReference
Include="..\..\src\LayeredCraft.DynamoMapper.Runtime\LayeredCraft.DynamoMapper.Runtime.csproj"
ReferenceOutputAssembly="true"
OutputItemType="Analyzer"
/>
</ItemGroup>
</Project>
52 changes: 52 additions & 0 deletions examples/DynamoMapper.Inheritance/Program.cs
Original file line number Diff line number Diff line change
@@ -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<string, AttributeValue> ToItem(Order source);

internal static partial Order FromItem(Dictionary<string, AttributeValue> item);
}

[DynamoMapper(IncludeBaseClassProperties = true)]
internal static partial class OrderMapper_WithBaseProps
{
internal static partial Dictionary<string, AttributeValue> ToItem(Order source);

internal static partial Order FromItem(Dictionary<string, AttributeValue> 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 + "'");
}
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 28 additions & 13 deletions src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ GeneratorContext context
{
context.ThrowIfCancellationRequested();

var properties = GetModelProperties(modelTypeSymbol);
var properties = GetModelProperties(modelTypeSymbol, context);

var varName = GetModelVarName(modelTypeSymbol, fromItemParameterName, context);

Expand Down Expand Up @@ -71,15 +71,29 @@ GeneratorContext context
}
}

private static IPropertySymbol[] GetModelProperties(ITypeSymbol modelTypeSymbol) =>
modelTypeSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => IsMappableProperty(p, modelTypeSymbol))
.ToArray();
private static IPropertySymbol[] GetModelProperties(
ITypeSymbol modelTypeSymbol,
GeneratorContext context
)
{
if (modelTypeSymbol is not INamedTypeSymbol namedType)
{
return modelTypeSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.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,
Expand Down Expand Up @@ -264,10 +278,11 @@ GeneratorContext context
var segment = segments[i];

// Find the property on the current type
var property = currentType
.GetMembers()
.OfType<IPropertySymbol>()
.FirstOrDefault(p => p.Name == segment);
var property = PropertySymbolLookup.FindPropertyByName(
currentType,
segment,
context.MapperOptions.IncludeBaseClassProperties
);

if (property == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -133,15 +133,16 @@ private static bool IsWellKnownNonNestedType(INamedTypeSymbol type, GeneratorCon
/// <summary>
/// Gets the mappable properties from a type.
/// </summary>
private static IPropertySymbol[] GetMappableProperties(INamedTypeSymbol type)
{
return type
.GetMembers()
.OfType<IPropertySymbol>()
.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")
);

/// <summary>
/// Analyzes a type for inline code generation, recursively building property specs.
Expand All @@ -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<NestedPropertySpec>();
var diagnostics = new List<DiagnosticInfo>();

Expand Down
Loading
Loading