Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5a0db01
feat(mappers): add helper methods for nested object mappings
j-d-ha Feb 16, 2026
4fa03d4
feat(emitters): optimize dictionary initialization with calculated ca…
j-d-ha Feb 16, 2026
8f7d584
feat(tests): add test for multi-level nested object mappings
j-d-ha Feb 16, 2026
d2a684d
feat(emitters): refactor to support reusable helper methods for neste…
j-d-ha Feb 16, 2026
d9c49b6
feat(tests): update snapshots to use helper methods for nested object…
j-d-ha Feb 16, 2026
7ee3078
refactor(emitters): switch helper methods to use expression-bodied me…
j-d-ha Feb 16, 2026
a322237
feat(mappers): enhance nested object property analysis and add requir…
j-d-ha Feb 17, 2026
b3386dc
feat(mappers): improve requiredness handling and support post-constru…
j-d-ha Feb 17, 2026
bfec277
feat(models): enhance `MapperInfo` with equality and hash code implem…
j-d-ha Feb 17, 2026
29854dd
feat(diagnostics): add diagnostic for helper rendering iteration limit
j-d-ha Feb 17, 2026
16ab080
fix(emitters): handle braces in depth tracking for helper methods
j-d-ha Feb 17, 2026
fc22b6a
feat(emitters): refine `.Set*` method detection and extraction logic
j-d-ha Feb 17, 2026
1f53c93
feat(property-mapping): centralize type name extraction and sanitizat…
j-d-ha Feb 17, 2026
675e5fc
feat(emitters): optimize property mapping rendering logic
j-d-ha Feb 17, 2026
e48f732
chore(deps): update Verify.XunitV3 to version 31.13.0
ncipollina Feb 17, 2026
1f8c747
refactor(models): make `MapperInfo` sealed and adjust equality implem…
j-d-ha Feb 18, 2026
02d1861
Merge remote-tracking branch 'origin/feature/format-nested-mappings' …
j-d-ha Feb 18, 2026
af6fb94
chore: update version prefix to 1.0.4 and SDK version to 10.0.103
ncipollina Feb 18, 2026
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.3</VersionPrefix>
<VersionPrefix>1.0.4</VersionPrefix>
<!-- SPDX license identifier for MIT -->
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<!-- Other useful metadata -->
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -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.12.4" />
<PackageVersion Include="Verify.XunitV3" Version="31.13.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3.mtp-v2" Version="3.2.2" />
</ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions LayeredCraft.DynamoMapper.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue">ExpressionBody</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/TRAILING_COMMA_IN_MULTILINE_LISTS/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/USE_HEURISTICS_FOR_BODY_STYLE/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_BINARY_EXPRESSIONS_CHAIN/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_EXPR_MEMBER_ARRANGEMENT/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_INITIALIZER_ARRANGEMENT/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_INVOCATION_PARENS_ARRANGEMENT/@EntryValue">False</s:Boolean>
Expand Down
1 change: 1 addition & 0 deletions LayeredCraft.DynamoMapper.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
<Folder Name="/Solution Items/">
<File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" />
<File Path="global.json" />
<File Path="README.md" />
<File Path="taskfile.yaml" />
</Folder>
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sdk": {
"rollForward": "latestMinor",
"version": "10.0.102"
"version": "10.0.103"
},
"test": {
"runner": "Microsoft.Testing.Platform"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
DM0006 | DynamoMapper.Usage | Error | DiagnosticDescriptors
DM0007 | DynamoMapper.Usage | Error | DiagnosticDescriptors
DM0008 | DynamoMapper.Usage | Error | DiagnosticDescriptors
DM0009 | DynamoMapper.Usage | Error | DiagnosticDescriptors
DM0101 | DynamoMapper.Usage | Error | DiagnosticDescriptors
DM0102 | DynamoMapper.Usage | Error | DiagnosticDescriptors
DM0103 | DynamoMapper.Usage | Error | DiagnosticDescriptors
Original file line number Diff line number Diff line change
Expand Up @@ -6,93 +6,113 @@ internal static class DiagnosticDescriptors
{
private const string UsageCategory = "DynamoMapper.Usage";

internal static readonly DiagnosticDescriptor CannotConvertFromAttributeValue = new(
"DM0001",
"Type cannot be mapped to an AttributeValue",
"The property '{0}' of type '{1}' cannot be mapped to an AttributeValue",
UsageCategory,
DiagnosticSeverity.Error,
true
);
internal static readonly DiagnosticDescriptor CannotConvertFromAttributeValue =
new(
"DM0001",
"Type cannot be mapped to an AttributeValue",
"The property '{0}' of type '{1}' cannot be mapped to an AttributeValue",
UsageCategory,
DiagnosticSeverity.Error,
true
);

internal static readonly DiagnosticDescriptor UnsupportedCollectionElementType = new(
"DM0003",
"Collection element type not supported",
"The property '{0}' has element type '{1}' which is not supported. Only primitive types are supported as collection elements.",
UsageCategory,
DiagnosticSeverity.Error,
true
);
internal static readonly DiagnosticDescriptor UnsupportedCollectionElementType =
new(
"DM0003",
"Collection element type not supported",
"The property '{0}' has element type '{1}' which is not supported. Only primitive types are supported as collection elements.",
UsageCategory,
DiagnosticSeverity.Error,
true
);

internal static readonly DiagnosticDescriptor DictionaryKeyMustBeString = new(
"DM0004",
"Dictionary key must be string",
"The property '{0}' has key type '{1}' but dictionary keys must be of type 'string'",
UsageCategory,
DiagnosticSeverity.Error,
true
);
internal static readonly DiagnosticDescriptor DictionaryKeyMustBeString =
new(
"DM0004",
"Dictionary key must be string",
"The property '{0}' has key type '{1}' but dictionary keys must be of type 'string'",
UsageCategory,
DiagnosticSeverity.Error,
true
);

internal static readonly DiagnosticDescriptor IncompatibleKindOverride = new(
"DM0005",
"Incompatible DynamoKind override for collection",
"The property '{0}' has Kind override '{1}' which is incompatible with the inferred collection kind '{2}'",
UsageCategory,
DiagnosticSeverity.Error,
true
);
internal static readonly DiagnosticDescriptor IncompatibleKindOverride =
new(
"DM0005",
"Incompatible DynamoKind override for collection",
"The property '{0}' has Kind override '{1}' which is incompatible with the inferred collection kind '{2}'",
UsageCategory,
DiagnosticSeverity.Error,
true
);

internal static readonly DiagnosticDescriptor NoMapperMethodsFound = new(
"DM0101",
"No mapper methods found",
"The mapper class '{0}' must define at least one partial method starting with 'To' or 'From'",
UsageCategory,
DiagnosticSeverity.Error,
true
);
internal static readonly DiagnosticDescriptor NoMapperMethodsFound =
new(
"DM0101",
"No mapper methods found",
"The mapper class '{0}' must define at least one partial method starting with 'To' or 'From'",
UsageCategory,
DiagnosticSeverity.Error,
true
);

internal static readonly DiagnosticDescriptor MismatchedPocoTypes = new(
"DM0102",
"Mapper methods use different POCO types",
"The mapper methods '{0}' and '{1}' must use the same POCO type, but '{2}' and '{3}' were found",
UsageCategory,
DiagnosticSeverity.Error,
true
);
internal static readonly DiagnosticDescriptor MismatchedPocoTypes =
new(
"DM0102",
"Mapper methods use different POCO types",
"The mapper methods '{0}' and '{1}' must use the same POCO type, but '{2}' and '{3}' were found",
UsageCategory,
DiagnosticSeverity.Error,
true
);

internal static readonly DiagnosticDescriptor MultipleConstructorsWithAttribute = new(
"DM0103",
"Multiple constructors marked with [DynamoMapperConstructor]",
"The type '{0}' has multiple constructors marked with [DynamoMapperConstructor]. Only one constructor can be marked with this attribute.",
UsageCategory,
DiagnosticSeverity.Error,
true
);
internal static readonly DiagnosticDescriptor MultipleConstructorsWithAttribute =
new(
"DM0103",
"Multiple constructors marked with [DynamoMapperConstructor]",
"The type '{0}' has multiple constructors marked with [DynamoMapperConstructor]. Only one constructor can be marked with this attribute.",
UsageCategory,
DiagnosticSeverity.Error,
true
);

internal static readonly DiagnosticDescriptor CycleDetectedInNestedType = new(
"DM0006",
"Circular reference detected in nested type",
"The property '{0}' creates a circular reference with type '{1}'. Cycles are not supported in nested object mapping.",
UsageCategory,
DiagnosticSeverity.Error,
true
);
internal static readonly DiagnosticDescriptor CycleDetectedInNestedType =
new(
"DM0006",
"Circular reference detected in nested type",
"The property '{0}' creates a circular reference with type '{1}'. Cycles are not supported in nested object mapping.",
UsageCategory,
DiagnosticSeverity.Error,
true
);

internal static readonly DiagnosticDescriptor UnsupportedNestedMemberType = new(
"DM0007",
"Unsupported nested member type",
"The nested property '{0}.{1}' has type '{2}' which cannot be mapped",
UsageCategory,
DiagnosticSeverity.Error,
true
);
internal static readonly DiagnosticDescriptor UnsupportedNestedMemberType =
new(
"DM0007",
"Unsupported nested member type",
"The nested property '{0}.{1}' has type '{2}' which cannot be mapped",
UsageCategory,
DiagnosticSeverity.Error,
true
);

internal static readonly DiagnosticDescriptor InvalidDotNotationPath = new(
"DM0008",
"Invalid dot-notation path",
"The dot-notation path '{0}' is invalid. Property '{1}' not found on type '{2}'.",
UsageCategory,
DiagnosticSeverity.Error,
true
);
internal static readonly DiagnosticDescriptor InvalidDotNotationPath =
new(
"DM0008",
"Invalid dot-notation path",
"The dot-notation path '{0}' is invalid. Property '{1}' not found on type '{2}'.",
UsageCategory,
DiagnosticSeverity.Error,
true
);

internal static readonly DiagnosticDescriptor HelperRenderingLimitExceeded =
new(
"DM0009",
"Helper method rendering limit exceeded",
"Mapper '{0}' exceeded the maximum of {1} helper-method rendering iterations. This indicates a bug in the DynamoMapper source generator β€” please file an issue.",
UsageCategory,
DiagnosticSeverity.Error,
true
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Text;
using DynamoMapper.Generator.Models;
using DynamoMapper.Generator.PropertyMapping;

namespace DynamoMapper.Generator.Emitters;

/// <summary>Generates helper method code for nested object mapping.</summary>
internal static class HelperMethodEmitter
{
/// <summary>Renders a ToItem helper method.</summary>
public static string RenderToItemHelper(
HelperMethodInfo helper, GeneratorContext context, HelperMethodRegistry helperRegistry
)
{
var sb = new StringBuilder();
var paramName = TypeNameHelper.ToParameterName(helper.ModelFullyQualifiedType);

// Method signature with arrow syntax (no leading spaces - template handles base
// indentation)
sb.AppendLine(
$"private static Dictionary<string, AttributeValue> {helper.MethodName}({helper.ModelFullyQualifiedType} {paramName}) =>"
);
sb.Append(" ");

// Count properties that produce Set* calls for dictionary capacity pre-allocation
var capacity =
helper.InlineInfo.Properties.Count(
p => p.NestedMapping is not null || (p.Strategy is not null && p.HasGetter)
);
sb.Append($"new Dictionary<string, AttributeValue>({capacity})");

// Render each property call on its own indented line directly from structured data
foreach (var prop in helper.InlineInfo.Properties)
{
var call =
PropertyMappingCodeRenderer.RenderToItemPropertyCall(
prop,
paramName,
context,
helperRegistry
);
if (call is not null)
{
sb.AppendLine();
sb.Append(" ");
sb.Append(call);
}
}

sb.Append(";");
return sb.ToString();
}

/// <summary>Renders a FromItem helper method.</summary>
public static string RenderFromItemHelper(
HelperMethodInfo helper, GeneratorContext context, HelperMethodRegistry helperRegistry
)
{
var sb = new StringBuilder();
var mapParamName = "map";

// Method signature with block syntax (no leading spaces - template handles base
// indentation)
sb.AppendLine(
$"private static {helper.ModelFullyQualifiedType} {helper.MethodName}(Dictionary<string, AttributeValue> {mapParamName})"
);
sb.AppendLine("{");

// Determine rendering path from structured data, without an intermediate string
var hasPostConstructionProperties =
helper.InlineInfo.Properties.Any(
p => !p.IsRequired && !p.IsInitOnly && p.HasDefaultValue && p.HasSetter
);

if (!hasPostConstructionProperties)
{
// Simple path: all properties go into the object initializer β€” build directly
sb.Append(" return ");
sb.AppendLine($"new {helper.ModelFullyQualifiedType}");
sb.AppendLine(" {");

foreach (var prop in helper.InlineInfo.Properties)
{
sb.Append(" ");
PropertyMappingCodeRenderer.RenderPropertyInitAssignment(
prop,
mapParamName,
sb,
context,
helperRegistry
);
sb.AppendLine();
}

sb.Append(" }");
sb.AppendLine(";");
}
else
{
// Complex path: mix of init and post-construction assignments.
// RenderInlineNestedFromItem already emits properly indented multi-line code.
var bodyCode =
PropertyMappingCodeRenderer.RenderInlineNestedFromItem(
mapParamName,
helper.InlineInfo,
context,
helperRegistry
);
sb.Append(bodyCode);
sb.AppendLine();
}

sb.AppendLine("}");

return sb.ToString();
}
}
Loading