diff --git a/docs/docs/configuration/conversions.md b/docs/docs/configuration/conversions.md
index d3ef71228b..44dfe121c0 100644
--- a/docs/docs/configuration/conversions.md
+++ b/docs/docs/configuration/conversions.md
@@ -14,6 +14,7 @@ Mapperly implements several types of automatic conversions (in order of priority
| Dictionary | Maps a source dictionary to an enumerable target | Source type is an `IDictionary<,>` or an `IReadOnlyDictionary<,>` |
| Enumerable | Maps an enumerable source to an enumerable target | Source type is an `IEnumerable<>` |
| Span | Maps a `Span<>`, `ReadOnlySpan<>` to or from `Span<>`, `ReadOnlySpan<>` or enumerable | Source or target type is a `Span<>`, `ReadOnlySpan<>` |
+| Tuple | Create a new instance of a `ValueTuple` or tuple expression i.e. `(10, 12)` | Target type is a `ValueTuple<>` or tuple expression |
| Memory | Maps a `Memory<>`, `ReadOnlyMemory<>` to or from `Memory<>`, `ReadOnlyMemory<>`, `Span<>`, `ReadOnlySpan<>` or enumerable | Source or target type is a `Memory<>` or `ReadOnlyMemory<>` |
| Implicit cast | Implicit cast operator | An implicit cast operator is defined to cast from the source type to the target type |
| Parse method | Uses a static `Parse` method on the target type | Source type is a `string` and target has a static method with the following signature: `TTarget Parse(string)`. |
diff --git a/src/Riok.Mapperly.Abstractions/MappingConversionType.cs b/src/Riok.Mapperly.Abstractions/MappingConversionType.cs
index 32b161ed68..a50319e3c7 100644
--- a/src/Riok.Mapperly.Abstractions/MappingConversionType.cs
+++ b/src/Riok.Mapperly.Abstractions/MappingConversionType.cs
@@ -107,6 +107,13 @@ public enum MappingConversionType
///
Memory = 1 << 14,
+ ///
+ /// If the target is a or tuple expression (A: 10, B: 12).
+ /// Supports positional and named mapping.
+ /// Only uses in .
+ ///
+ Tuple = 1 << 15,
+
///
/// Enables all supported conversions.
///
diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
index 0bf47f01ee..4c37c1c801 100644
--- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
+++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
@@ -78,6 +78,7 @@ Riok.Mapperly.Abstractions.MappingConversionType.ParseMethod = 8 -> Riok.Mapperl
Riok.Mapperly.Abstractions.MappingConversionType.Span = 8192 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.StringToEnum = 32 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.ToStringMethod = 16 -> Riok.Mapperly.Abstractions.MappingConversionType
+Riok.Mapperly.Abstractions.MappingConversionType.Tuple = 32768 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.Queryable = 1024 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.get -> bool
Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.set -> void
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewValueTupleBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewValueTupleBuilderContext.cs
new file mode 100644
index 0000000000..e92dca4548
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewValueTupleBuilderContext.cs
@@ -0,0 +1,15 @@
+using Riok.Mapperly.Descriptors.Mappings;
+using Riok.Mapperly.Descriptors.Mappings.MemberMappings;
+
+namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;
+
+///
+/// A for mappings which create the target object
+/// via a tuple expression (eg. (A: source.A, B: MapToB(source.B))).
+///
+/// The mapping type.
+public interface INewValueTupleBuilderContext : IMembersBuilderContext
+ where T : IMapping
+{
+ void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping);
+}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs
new file mode 100644
index 0000000000..1d93bddf1b
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs
@@ -0,0 +1,22 @@
+using Riok.Mapperly.Descriptors.Mappings;
+using Riok.Mapperly.Descriptors.Mappings.MemberMappings;
+
+namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;
+
+///
+/// An implementation of .
+///
+/// The type of the mapping.
+public class NewValueTupleConstructorBuilderContext : MembersMappingBuilderContext, INewValueTupleBuilderContext
+ where T : INewValueTupleMapping
+{
+ public NewValueTupleConstructorBuilderContext(MappingBuilderContext builderContext, T mapping)
+ : base(builderContext, mapping) { }
+
+ public void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping)
+ {
+ MemberConfigsByRootTargetName.Remove(mapping.Parameter.Name);
+ SetSourceMemberMapped(mapping.DelegateMapping.SourcePath);
+ Mapping.AddConstructorParameterMapping(mapping);
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs
new file mode 100644
index 0000000000..31468863a8
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs
@@ -0,0 +1,34 @@
+using Riok.Mapperly.Descriptors.Mappings;
+using Riok.Mapperly.Descriptors.Mappings.MemberMappings;
+
+namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;
+
+///
+/// An implementation of .
+///
+/// The type of the mapping.
+public class NewValueTupleExpressionBuilderContext : MembersContainerBuilderContext, INewValueTupleBuilderContext
+ where T : INewValueTupleMapping, IMemberAssignmentTypeMapping
+{
+ public NewValueTupleExpressionBuilderContext(MappingBuilderContext builderContext, T mapping)
+ : base(builderContext, mapping) { }
+
+ public void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping)
+ {
+ if (MemberConfigsByRootTargetName.TryGetValue(mapping.Parameter.Name, out var value))
+ {
+ // remove the mapping used to map the tuple constructor
+ value.RemoveAll(x => x.Target.Path.Count == 1);
+
+ // remove form dictionary if there aren't nested mappings
+ if (!value.Any())
+ {
+ MemberConfigsByRootTargetName.Remove(mapping.Parameter.Name);
+ TargetMembers.Remove(mapping.Parameter.Name);
+ }
+ }
+
+ SetSourceMemberMapped(mapping.DelegateMapping.SourcePath);
+ Mapping.AddConstructorParameterMapping(mapping);
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MappingBodyBuilder.cs
index 8fa3f00523..30820e3b15 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MappingBodyBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MappingBodyBuilder.cs
@@ -28,6 +28,12 @@ public void BuildMappingBodies()
case NewInstanceObjectMemberMapping mapping:
NewInstanceObjectMemberMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
+ case NewValueTupleExpressionMapping mapping:
+ NewValueTupleMappingBodyBuilder.BuildMappingBody(ctx, mapping);
+ break;
+ case NewValueTupleConstructorMapping mapping:
+ NewValueTupleMappingBodyBuilder.BuildMappingBody(ctx, mapping);
+ break;
case IMemberAssignmentTypeMapping mapping:
ObjectMemberMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs
new file mode 100644
index 0000000000..b9112ce6c6
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs
@@ -0,0 +1,227 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.CodeAnalysis;
+using Riok.Mapperly.Abstractions;
+using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;
+using Riok.Mapperly.Descriptors.Mappings;
+using Riok.Mapperly.Descriptors.Mappings.MemberMappings;
+using Riok.Mapperly.Diagnostics;
+using Riok.Mapperly.Helpers;
+using Riok.Mapperly.Symbols;
+
+namespace Riok.Mapperly.Descriptors.MappingBodyBuilders;
+
+public static class NewValueTupleMappingBodyBuilder
+{
+ public static void BuildMappingBody(MappingBuilderContext ctx, NewValueTupleConstructorMapping expressionMapping)
+ {
+ var mappingCtx = new NewValueTupleConstructorBuilderContext(ctx, expressionMapping);
+ BuildTupleConstructorMapping(mappingCtx);
+ mappingCtx.AddDiagnostics();
+ }
+
+ public static void BuildMappingBody(MappingBuilderContext ctx, NewValueTupleExpressionMapping expressionMapping)
+ {
+ var mappingCtx = new NewValueTupleExpressionBuilderContext(ctx, expressionMapping);
+ BuildTupleConstructorMapping(mappingCtx);
+ ObjectMemberMappingBodyBuilder.BuildMappingBody(mappingCtx);
+ mappingCtx.AddDiagnostics();
+ }
+
+ private static void BuildTupleConstructorMapping(INewValueTupleBuilderContext ctx)
+ {
+ if (ctx.Mapping.TargetType is not INamedTypeSymbol namedTargetType)
+ {
+ ctx.BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoConstructorFound, ctx.BuilderContext.Target);
+ return;
+ }
+
+ if (!TryBuildTupleConstructorMapping(ctx, namedTargetType, out var constructorParameterMappings, out var mappedTargetMemberNames))
+ {
+ ctx.BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoConstructorFound, ctx.BuilderContext.Target);
+ return;
+ }
+
+ var removableMappedTargetMemberNames = mappedTargetMemberNames.Where(x => !ctx.MemberConfigsByRootTargetName.ContainsKey(x));
+
+ ctx.TargetMembers.RemoveRange(removableMappedTargetMemberNames);
+ foreach (var constructorParameterMapping in constructorParameterMappings)
+ {
+ ctx.AddTupleConstructorParameterMapping(constructorParameterMapping);
+ }
+ }
+
+ private static bool TryBuildTupleConstructorMapping(
+ INewValueTupleBuilderContext ctx,
+ INamedTypeSymbol namedTargetType,
+ out HashSet constructorParameterMappings,
+ out HashSet mappedTargetMemberNames
+ )
+ {
+ mappedTargetMemberNames = new HashSet();
+ constructorParameterMappings = new HashSet();
+
+ foreach (var targetMember in namedTargetType.TupleElements)
+ {
+ if (!ctx.TargetMembers.ContainsKey(targetMember.Name))
+ {
+ return false;
+ }
+
+ if (!TryFindConstructorParameterSourcePath(ctx, targetMember, out var sourcePath))
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.SourceMemberNotFound,
+ targetMember.Name,
+ ctx.Mapping.TargetType,
+ ctx.Mapping.SourceType
+ );
+
+ return false;
+ }
+
+ // nullability is handled inside the member expressionMapping
+ var paramType = targetMember.Type.WithNullableAnnotation(targetMember.NullableAnnotation);
+ var delegateMapping =
+ ctx.BuilderContext.FindMapping(sourcePath.MemberType, paramType)
+ ?? ctx.BuilderContext.FindOrBuildMapping(sourcePath.Member.Type.NonNullable(), paramType.NonNullable());
+
+ if (delegateMapping == null)
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.CouldNotMapMember,
+ ctx.Mapping.SourceType,
+ sourcePath.FullName,
+ sourcePath.Member.Type,
+ ctx.Mapping.TargetType,
+ targetMember.Name,
+ targetMember.Type
+ );
+ return false;
+ }
+
+ if (delegateMapping.Equals(ctx.Mapping))
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.ReferenceLoopInCtorMapping,
+ ctx.Mapping.SourceType,
+ sourcePath.FullName,
+ ctx.Mapping.TargetType,
+ targetMember.Name
+ );
+ return false;
+ }
+
+ var memberMapping = new NullMemberMapping(
+ delegateMapping,
+ sourcePath,
+ paramType,
+ ctx.BuilderContext.GetNullFallbackValue(paramType),
+ !ctx.BuilderContext.IsExpression
+ );
+
+ var ctorMapping = new ValueTupleConstructorParameterMapping(targetMember, memberMapping);
+ constructorParameterMappings.Add(ctorMapping);
+ mappedTargetMemberNames.Add(targetMember.Name);
+ }
+
+ return true;
+ }
+
+ private static bool TryFindConstructorParameterSourcePath(
+ INewValueTupleBuilderContext ctx,
+ IFieldSymbol field,
+ [NotNullWhen(true)] out MemberPath? sourcePath
+ )
+ {
+ sourcePath = null;
+
+ if (!ctx.MemberConfigsByRootTargetName.TryGetValue(field.Name, out var memberConfigs))
+ {
+ return TryBuildConstructorParameterSourcePath(ctx, field, out sourcePath);
+ }
+
+ // remove nested targets
+ var initMemberPaths = memberConfigs.Where(x => x.Target.Path.Count == 1).ToArray();
+
+ // if all memberConfigs are nested than do normal mapping
+ if (initMemberPaths.Length == 0)
+ {
+ return TryBuildConstructorParameterSourcePath(ctx, field, out sourcePath);
+ }
+
+ if (initMemberPaths.Length > 1)
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.MultipleConfigurationsForConstructorParameter,
+ field.Type,
+ field.Name
+ );
+ }
+
+ var memberConfig = initMemberPaths.First();
+
+ if (!ctx.BuilderContext.SymbolAccessor.TryFindMemberPath(ctx.Mapping.SourceType, memberConfig.Source.Path, out sourcePath))
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.SourceMemberNotFound,
+ memberConfig.Source,
+ ctx.Mapping.TargetType,
+ ctx.Mapping.SourceType
+ );
+ return false;
+ }
+
+ return true;
+ }
+
+ private static bool TryBuildConstructorParameterSourcePath(
+ INewValueTupleBuilderContext ctx,
+ IFieldSymbol field,
+ out MemberPath? sourcePath
+ )
+ {
+ var memberNameComparer =
+ ctx.BuilderContext.MapperConfiguration.PropertyNameMappingStrategy == PropertyNameMappingStrategy.CaseSensitive
+ ? StringComparer.Ordinal
+ : StringComparer.OrdinalIgnoreCase;
+
+ if (
+ ctx.BuilderContext.SymbolAccessor.TryFindMemberPath(
+ ctx.Mapping.SourceType,
+ MemberPathCandidateBuilder.BuildMemberPathCandidates(field.Name),
+ ctx.IgnoredSourceMemberNames,
+ memberNameComparer,
+ out sourcePath
+ )
+ )
+ return true;
+
+ // if standard matching fails, use the underlying field
+ // if source is a tuple compare the underlying field ie, Item1, Item2
+ // if source is not a tuple compare top level members
+ if (ctx.Mapping.SourceType.IsTupleType)
+ {
+ if (ctx.Mapping.SourceType is not INamedTypeSymbol namedType)
+ return false;
+
+ var mappableMember = namedType.TupleElements
+ .Where(
+ x =>
+ x.CorrespondingTupleField != default
+ && !ctx.IgnoredSourceMemberNames.Contains(x.Name)
+ && string.Equals(field.CorrespondingTupleField!.Name, x.CorrespondingTupleField!.Name)
+ )
+ .Select(MappableMember.Create)
+ .WhereNotNull()
+ .FirstOrDefault();
+
+ if (mappableMember != default)
+ {
+ sourcePath = new MemberPath(new[] { mappableMember });
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs
index 756479c60d..63ff14dd5a 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs
@@ -16,6 +16,12 @@ public static class ExplicitCastMappingBuilder
if (ctx.MapperConfiguration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
return null;
+ // ClassifyConversion does not check if tuple field member names are the same
+ // if tuple check isn't done then (A: int, B: int) -> (B: int, A: int) would be mapped
+ // return ((int, int))source; instead of return (B: source.A, A: source.B);
+ if (ctx.Target.IsTupleType)
+ return null;
+
if (SymbolEqualityComparer.Default.Equals(ctx.Source, ctx.Compilation.ObjectType))
return null;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs
index 4e9e93a672..2945843908 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs
@@ -15,6 +15,12 @@ public static class ImplicitCastMappingBuilder
if (ctx.MapperConfiguration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
return null;
+ // ClassifyConversion does not check if tuple field member names are the same
+ // if tuple check isn't done then (A: int, B: int) -> (B: int, A: int) would be mapped
+ // return source; instead of return (B: source.A, A: source.B);
+ if (ctx.Target.IsTupleType)
+ return null;
+
var conversion = ctx.Compilation.ClassifyConversion(ctx.Source, ctx.Target);
return conversion.IsImplicit ? new CastMapping(ctx.Source, ctx.Target) : null;
}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs
index c17ef13287..97a3952494 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
+using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Helpers;
@@ -26,6 +27,21 @@ public static class NewInstanceObjectPropertyMappingBuilder
if (ctx.Source.IsEnum() || ctx.Target.IsEnum())
return null;
+ if (ctx.Target.IsTupleType)
+ {
+ if (!ctx.IsConversionEnabled(MappingConversionType.Tuple))
+ return null;
+
+ // inline expressions don't support tuple expressions so ValueTuple is used instead
+ if (ctx.IsExpression)
+ {
+ return new NewValueTupleConstructorMapping(ctx.Source, ctx.Target);
+ }
+
+ var expectedArgumentCount = (ctx.Target as INamedTypeSymbol)!.TupleElements.Length;
+ return new NewValueTupleExpressionMapping(ctx.Source, ctx.Target, expectedArgumentCount);
+ }
+
// inline expressions don't support method property mappings
// and can only map to properties via object initializers.
return ctx.IsExpression
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/INewValueTupleMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/INewValueTupleMapping.cs
new file mode 100644
index 0000000000..95e25e465c
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/INewValueTupleMapping.cs
@@ -0,0 +1,11 @@
+using Riok.Mapperly.Descriptors.Mappings.MemberMappings;
+
+namespace Riok.Mapperly.Descriptors.Mappings;
+
+///
+/// A tuple mapping creating the target instance via a tuple expression (eg. (A: 10, B: 20)).
+///
+public interface INewValueTupleMapping : IMapping
+{
+ void AddConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping);
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs
index 11dc875f96..3cefa16245 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs
@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
@@ -22,8 +22,8 @@ public ConstructorParameterMapping(IParameterSymbol parameter, NullMemberMapping
public ArgumentSyntax BuildArgument(TypeMappingBuildContext ctx)
{
var argumentExpression = DelegateMapping.Build(ctx);
- var arg = SyntaxFactory.Argument(argumentExpression);
- return _selfOrPreviousIsUnmappedOptional ? arg.WithNameColon(SyntaxFactory.NameColon(Parameter.Name)) : arg;
+ var arg = Argument(argumentExpression);
+ return _selfOrPreviousIsUnmappedOptional ? arg.WithNameColon(NameColon(Parameter.Name)) : arg;
}
protected bool Equals(ConstructorParameterMapping other) =>
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMappingContainer.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMappingContainer.cs
index ce4274699a..e4f8cd84d6 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMappingContainer.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMappingContainer.cs
@@ -7,6 +7,8 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
///
public interface IMemberAssignmentMappingContainer
{
+ bool HasMemberMappings();
+
bool HasMemberMapping(IMemberAssignmentMapping mapping);
void AddMemberMapping(IMemberAssignmentMapping mapping);
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMappingContainer.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMappingContainer.cs
index 87744146f1..f3a0b44a2f 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMappingContainer.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMappingContainer.cs
@@ -42,6 +42,8 @@ public void AddMemberMapping(IMemberAssignmentMapping mapping)
}
}
+ public bool HasMemberMappings() => _delegateMappings.Any() || _childContainers.Any(x => x.HasMemberMappings());
+
public bool HasMemberMapping(IMemberAssignmentMapping mapping) =>
_delegateMappings.Contains(mapping) || _parent?.HasMemberMapping(mapping) == true;
}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs
new file mode 100644
index 0000000000..d9e7a49e4a
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs
@@ -0,0 +1,67 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
+
+public class ValueTupleConstructorParameterMapping
+{
+ public ValueTupleConstructorParameterMapping(IFieldSymbol parameter, NullMemberMapping delegateMapping)
+ {
+ DelegateMapping = delegateMapping;
+ Parameter = parameter;
+ }
+
+ public IFieldSymbol Parameter { get; }
+
+ public NullMemberMapping DelegateMapping { get; }
+
+ public ArgumentSyntax BuildArgument(TypeMappingBuildContext ctx, bool emitFieldName)
+ {
+ var argumentExpression = DelegateMapping.Build(ctx);
+ var argument = Argument(argumentExpression);
+
+ // tuples inside expression cannot use the expression form (A: .., ..) instead new ValueTuple<>(..) must be used
+ // custom field names cannot be used so we return a plain argument
+ if (!emitFieldName)
+ return argument;
+
+ // add field name if available
+ return SymbolEqualityComparer.Default.Equals(Parameter.CorrespondingTupleField, Parameter)
+ ? argument
+ : argument.WithNameColon(NameColon(IdentifierName(Parameter.Name)));
+ }
+
+ protected bool Equals(ValueTupleConstructorParameterMapping other) =>
+ Parameter.Equals(other.Parameter, SymbolEqualityComparer.Default) && DelegateMapping.Equals(other.DelegateMapping);
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(null, obj))
+ return false;
+
+ if (ReferenceEquals(this, obj))
+ return true;
+
+ if (obj.GetType() != GetType())
+ return false;
+
+ return Equals((ValueTupleConstructorParameterMapping)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = SymbolEqualityComparer.Default.GetHashCode(Parameter);
+ hashCode = (hashCode * 397) ^ DelegateMapping.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ public static bool operator ==(ValueTupleConstructorParameterMapping? left, ValueTupleConstructorParameterMapping? right) =>
+ Equals(left, right);
+
+ public static bool operator !=(ValueTupleConstructorParameterMapping? left, ValueTupleConstructorParameterMapping? right) =>
+ !Equals(left, right);
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
index a5852717cc..0e771ed980 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
@@ -63,7 +63,7 @@ ITypeSymbol targetType
public override ExpressionSyntax Build(TypeMappingBuildContext ctx) =>
Invocation(MethodName, SourceParameter.WithArgument(ctx.Source), ReferenceHandlerParameter?.WithArgument(ctx.ReferenceHandler));
- public virtual MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx)
+ public virtual MethodDeclarationSyntax? BuildMethod(SourceEmitterContext ctx)
{
var returnType = FullyQualifiedIdentifier(_returnType);
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleConstructorMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleConstructorMapping.cs
new file mode 100644
index 0000000000..ee01fdd11e
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleConstructorMapping.cs
@@ -0,0 +1,33 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Descriptors.Mappings.MemberMappings;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.Mappings;
+
+///
+/// An object mapping creating the target instance via new ValueTuple<int, string>(source.A, source.B),
+/// mapping properties via ctor, but not by assigning.
+///
+///
+public class NewValueTupleConstructorMapping : TypeMapping, INewValueTupleMapping
+{
+ private const string ValueTupleName = "global::System.ValueTuple";
+ private readonly HashSet _constructorPropertyMappings = new();
+
+ public NewValueTupleConstructorMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
+ : base(sourceType, targetType) { }
+
+ public void AddConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping) => _constructorPropertyMappings.Add(mapping);
+
+ public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
+ {
+ // new ValueTuple(ctorArgs)
+ var ctorArgs = _constructorPropertyMappings.Select(x => x.BuildArgument(ctx, emitFieldName: false));
+ var genericName = GenericName(ValueTupleName);
+ var typeArguments = TypeArgumentList(((INamedTypeSymbol)TargetType).TypeArguments.Select(NonNullableIdentifier));
+ var typedValue = genericName.WithTypeArgumentList(typeArguments);
+ return ObjectCreationExpression(typedValue).WithArgumentList(ArgumentList(ctorArgs));
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleExpressionMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleExpressionMapping.cs
new file mode 100644
index 0000000000..850bd15976
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleExpressionMapping.cs
@@ -0,0 +1,91 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Descriptors.Mappings.MemberMappings;
+using Riok.Mapperly.Emit;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.Mappings;
+
+///
+/// An object mapping creating the target instance via a tuple expression (eg. (A: 10, B: 20)),
+/// mapping properties via ctor, not by assigning.
+///
+///
+public class NewValueTupleExpressionMapping : ObjectMemberMethodMapping, INewValueTupleMapping
+{
+ private readonly int _argumentCount;
+ private const string NoMappingComment = "// Could not generate mapping";
+ private const string TargetVariableName = "target";
+ private readonly HashSet _constructorPropertyMappings = new();
+
+ public NewValueTupleExpressionMapping(ITypeSymbol sourceType, ITypeSymbol targetType, int argumentCount)
+ : base(sourceType, targetType)
+ {
+ _argumentCount = argumentCount;
+ }
+
+ public void AddConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping) => _constructorPropertyMappings.Add(mapping);
+
+ public override MethodDeclarationSyntax? BuildMethod(SourceEmitterContext ctx)
+ {
+ // generate method body if complex mapping otherwise return null
+ return HasMemberMappings() ? base.BuildMethod(ctx) : null;
+ }
+
+ public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
+ {
+ // generate error if constructor argument don't match
+ if (_constructorPropertyMappings.Count != _argumentCount)
+ {
+ return ThrowNotImplementedException().WithLeadingTrivia(TriviaList(Comment(NoMappingComment)));
+ }
+
+ // generate method call if it's a complex mapping
+ if (HasMemberMappings())
+ {
+ return base.Build(ctx);
+ }
+
+ // (Name:.. ,..);
+ var ctorArgs = _constructorPropertyMappings.Select(x => x.BuildArgument(ctx, emitFieldName: true));
+ return TupleExpression(SeparatedList(ctorArgs));
+ }
+
+ public override IEnumerable BuildBody(TypeMappingBuildContext ctx)
+ {
+ // generate error if constructor argument don't match
+ if (_constructorPropertyMappings.Count != _argumentCount)
+ {
+ yield return ExpressionStatement(ThrowNotImplementedException()).WithLeadingTrivia(TriviaList(Comment(NoMappingComment)));
+ yield break;
+ }
+
+ // (Name:.. ,..);
+ var ctorArgs = _constructorPropertyMappings.Select(x => x.BuildArgument(ctx, emitFieldName: true));
+ var tupleCreationExpression = TupleExpression(SeparatedList(ctorArgs));
+
+ // return (Name:.. ,..); if not a complex mapping
+ if (!HasMemberMappings())
+ {
+ yield return ReturnStatement(tupleCreationExpression);
+ yield break;
+ }
+
+ // generate
+ // var target = (Name:.. ,..);
+ // target.Name.Child = ...
+ // return target
+ var targetVariableName = ctx.NameBuilder.New(TargetVariableName);
+ yield return DeclareLocalVariable(targetVariableName, tupleCreationExpression);
+
+ // map properties
+ foreach (var expression in BuildBody(ctx, IdentifierName(targetVariableName)))
+ {
+ yield return expression;
+ }
+
+ // return target;
+ yield return ReturnVariable(targetVariableName);
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ObjectMemberMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ObjectMemberMethodMapping.cs
index 0d5c4510e2..839ba53d0f 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/ObjectMemberMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/ObjectMemberMethodMapping.cs
@@ -19,6 +19,8 @@ protected ObjectMemberMethodMapping(ITypeSymbol sourceType, ITypeSymbol targetTy
_mapping = new ObjectMemberExistingTargetMapping(sourceType, targetType);
}
+ public bool HasMemberMappings() => _mapping.HasMemberMappings();
+
public bool HasMemberMapping(IMemberAssignmentMapping mapping) => _mapping.HasMemberMapping(mapping);
public void AddMemberMapping(IMemberAssignmentMapping mapping) => _mapping.AddMemberMapping(mapping);
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs
index dc5d36098e..d7833cb0a8 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs
@@ -30,8 +30,8 @@ ITypeSymbol objectType
public GenericMappingTypeParameters TypeParameters { get; }
- public override MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) =>
- base.BuildMethod(ctx).WithTypeParameterList(TypeParameterList(TypeParameters.SourceType, TypeParameters.TargetType));
+ public override MethodDeclarationSyntax? BuildMethod(SourceEmitterContext ctx) =>
+ base.BuildMethod(ctx)?.WithTypeParameterList(TypeParameterList(TypeParameters.SourceType, TypeParameters.TargetType));
protected override ExpressionSyntax BuildTargetType()
{
diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs
index a03d42e112..401d9e4580 100644
--- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs
+++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs
@@ -185,6 +185,11 @@ private IEnumerable GetAllMembersCore(ITypeSymbol symbol)
private IEnumerable GetAllAccessibleMappableMembersCore(ITypeSymbol symbol)
{
+ if (symbol.IsTupleType && symbol is INamedTypeSymbol namedType)
+ {
+ return namedType.TupleElements.Select(MappableMember.Create).WhereNotNull();
+ }
+
return GetAllMembers(symbol)
.Where(x => x is { IsStatic: false, Kind: SymbolKind.Property or SymbolKind.Field } && x.IsAccessible())
.DistinctBy(x => x.Name)
diff --git a/src/Riok.Mapperly/Emit/SourceEmitter.cs b/src/Riok.Mapperly/Emit/SourceEmitter.cs
index cba4250715..ee3bafb6ee 100644
--- a/src/Riok.Mapperly/Emit/SourceEmitter.cs
+++ b/src/Riok.Mapperly/Emit/SourceEmitter.cs
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors;
+using Riok.Mapperly.Helpers;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
@@ -23,7 +24,7 @@ public static CompilationUnitSyntax Build(MapperDescriptor descriptor)
private static IEnumerable BuildMembers(MapperDescriptor descriptor, SourceEmitterContext sourceEmitterContext)
{
- return descriptor.MethodTypeMappings.Select(mapping => mapping.BuildMethod(sourceEmitterContext));
+ return descriptor.MethodTypeMappings.Select(mapping => mapping.BuildMethod(sourceEmitterContext)).WhereNotNull();
}
private static MemberDeclarationSyntax WrapInClassesAsNeeded(INamedTypeSymbol symbol, MemberDeclarationSyntax syntax)
diff --git a/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs
index 14c74f4f61..f246e70295 100644
--- a/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs
+++ b/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs
@@ -396,8 +396,13 @@ public static ArgumentListSyntax ArgumentList(params ExpressionSyntax[] argSynta
public static TypeArgumentListSyntax TypeArgumentList(params TypeSyntax[] argSyntaxes) =>
SyntaxFactory.TypeArgumentList(CommaSeparatedList(argSyntaxes));
+ public static TypeArgumentListSyntax TypeArgumentList(IEnumerable argSyntaxes) =>
+ SyntaxFactory.TypeArgumentList(CommaSeparatedList(argSyntaxes));
+
public static ArgumentListSyntax ArgumentList(params ArgumentSyntax[] args) => SyntaxFactory.ArgumentList(CommaSeparatedList(args));
+ public static ArgumentListSyntax ArgumentList(IEnumerable args) => SyntaxFactory.ArgumentList(CommaSeparatedList(args));
+
public static SeparatedSyntaxList CommaSeparatedList(IEnumerable nodes, bool insertTrailingComma = false)
where T : SyntaxNode => SeparatedList(JoinByComma(nodes, insertTrailingComma));
diff --git a/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs
index 0eaa946613..a769805480 100644
--- a/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs
+++ b/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs
@@ -72,6 +72,7 @@ public static TestObject NewTestObj()
NullableFlattening = new() { IdValue = 100 },
UnflatteningIdValue = 20,
NullableUnflatteningIdValue = 200,
+ TupleValue = ("10", "20"),
RecursiveObject = new(5)
{
EnumValue = TestEnum.Value10,
diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
index 574d0fcb92..37c189a0fa 100644
--- a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
@@ -47,6 +47,8 @@ public TestObjectDto(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)
public string StringNullableTargetNotNullable { get; set; } = string.Empty;
+ public (int A, int)? TupleValue { get; set; }
+
public TestObjectDto? RecursiveObject { get; set; }
public TestObject? SourceTargetSameObjectType { get; set; }
diff --git a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
index c01df23838..9d69eda85f 100644
--- a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
@@ -44,6 +44,8 @@ public TestObject(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)
public string? StringNullableTargetNotNullable { get; set; }
+ public (string A, string)? TupleValue { get; set; }
+
public TestObject? RecursiveObject { get; set; }
public TestObject? SourceTargetSameObjectType { get; set; }
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.RunMappingShouldWork.verified.txt
index a4c1c704c6..699a80182d 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.RunMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.RunMappingShouldWork.verified.txt
@@ -19,6 +19,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs
index 1dd22355eb..89d6ee9a40 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs
@@ -33,6 +33,11 @@ public static partial class DeepCloningMapper
target.NestedNullableTargetNotNullable = MapToTestObjectNested(src.NestedNullableTargetNotNullable);
}
+ if (src.TupleValue != null)
+ {
+ target.TupleValue = (A: src.TupleValue.Value.A, src.TupleValue.Value.Item2);
+ }
+
if (src.RecursiveObject != null)
{
target.RecursiveObject = Copy(src.RecursiveObject);
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.RunMappingShouldWork.verified.txt
index 6f6e920259..a24ad94486 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.RunMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.RunMappingShouldWork.verified.txt
@@ -20,6 +20,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.SnapshotGeneratedSource.verified.cs
index 69c860dde3..7ea17191c5 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.SnapshotGeneratedSource.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.SnapshotGeneratedSource.verified.cs
@@ -78,6 +78,11 @@ public partial int ParseableInt(string value)
target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable;
}
+ if (testObject.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2));
+ }
+
if (testObject.RecursiveObject != null)
{
target.RecursiveObject = MapToDto(testObject.RecursiveObject);
@@ -157,6 +162,11 @@ public partial int ParseableInt(string value)
target.NestedNullable = MapToTestObjectNested(dto.NestedNullable);
}
+ if (dto.TupleValue != null)
+ {
+ target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString());
+ }
+
if (dto.RecursiveObject != null)
{
target.RecursiveObject = MapFromDto(dto.RecursiveObject);
@@ -243,6 +253,11 @@ public partial void UpdateDto(global::Riok.Mapperly.IntegrationTests.Models.Test
target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable;
}
+ if (source.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2));
+ }
+
if (source.RecursiveObject != null)
{
target.RecursiveObject = MapToDto(source.RecursiveObject);
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt
index d67b3bf99f..a5818925d2 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt
@@ -15,6 +15,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunMappingShouldWork.verified.txt
index 9196056a62..13dfd1453a 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunMappingShouldWork.verified.txt
@@ -20,6 +20,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs
index 6321bdf662..9af3f7e867 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs
@@ -77,6 +77,11 @@ public static partial int ParseableInt(string value)
target.StringNullableTargetNotNullable = src.StringNullableTargetNotNullable;
}
+ if (src.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(src.TupleValue.Value.A), ParseableInt(src.TupleValue.Value.Item2));
+ }
+
if (src.RecursiveObject != null)
{
target.RecursiveObject = MapToDtoExt(src.RecursiveObject);
@@ -171,6 +176,11 @@ public static partial int ParseableInt(string value)
target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable;
}
+ if (testObject.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2));
+ }
+
if (testObject.RecursiveObject != null)
{
target.RecursiveObject = MapToDtoExt(testObject.RecursiveObject);
@@ -250,6 +260,11 @@ public static partial int ParseableInt(string value)
target.NestedNullable = MapToTestObjectNested(dto.NestedNullable);
}
+ if (dto.TupleValue != null)
+ {
+ target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString());
+ }
+
if (dto.RecursiveObject != null)
{
target.RecursiveObject = MapFromDto(dto.RecursiveObject);
@@ -336,6 +351,11 @@ public static partial void UpdateDto(global::Riok.Mapperly.IntegrationTests.Mode
target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable;
}
+ if (source.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2));
+ }
+
if (source.RecursiveObject != null)
{
target.RecursiveObject = MapToDtoExt(source.RecursiveObject);
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.RunMappingShouldWork.verified.txt
index 90335d9b16..6fa816fe88 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.RunMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.RunMappingShouldWork.verified.txt
@@ -19,6 +19,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs
index 4bc910514f..eb9b754ab8 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs
@@ -30,6 +30,11 @@ public static partial class DeepCloningMapper
target.NestedNullableTargetNotNullable = MapToTestObjectNested(src.NestedNullableTargetNotNullable);
}
+ if (src.TupleValue != null)
+ {
+ target.TupleValue = (A: src.TupleValue.Value.A, src.TupleValue.Value.Item2);
+ }
+
if (src.RecursiveObject != null)
{
target.RecursiveObject = Copy(src.RecursiveObject);
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.RunMappingShouldWork.verified.txt
index 4d03e0e639..9c57b7ac72 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.RunMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.RunMappingShouldWork.verified.txt
@@ -20,6 +20,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.SnapshotGeneratedSource.verified.cs
index 63583c5592..9d9c49970a 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.SnapshotGeneratedSource.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.SnapshotGeneratedSource.verified.cs
@@ -75,6 +75,11 @@ public partial int ParseableInt(string value)
target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable;
}
+ if (testObject.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2));
+ }
+
if (testObject.RecursiveObject != null)
{
target.RecursiveObject = MapToDto(testObject.RecursiveObject);
@@ -153,6 +158,11 @@ public partial int ParseableInt(string value)
target.NestedNullable = MapToTestObjectNested(dto.NestedNullable);
}
+ if (dto.TupleValue != null)
+ {
+ target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString());
+ }
+
if (dto.RecursiveObject != null)
{
target.RecursiveObject = MapFromDto(dto.RecursiveObject);
@@ -241,6 +251,11 @@ public partial void UpdateDto(global::Riok.Mapperly.IntegrationTests.Models.Test
target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable;
}
+ if (source.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2));
+ }
+
if (source.RecursiveObject != null)
{
target.RecursiveObject = MapToDto(source.RecursiveObject);
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt
index d0a7271d51..3437d7e928 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt
@@ -15,6 +15,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunMappingShouldWork.verified.txt
index 5ade304416..06c3a7cec3 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunMappingShouldWork.verified.txt
@@ -20,6 +20,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs
index ab6f019a34..e77ec8de5d 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs
@@ -74,6 +74,11 @@ public static partial int ParseableInt(string value)
target.StringNullableTargetNotNullable = src.StringNullableTargetNotNullable;
}
+ if (src.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(src.TupleValue.Value.A), ParseableInt(src.TupleValue.Value.Item2));
+ }
+
if (src.RecursiveObject != null)
{
target.RecursiveObject = MapToDtoExt(src.RecursiveObject);
@@ -167,6 +172,11 @@ public static partial int ParseableInt(string value)
target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable;
}
+ if (testObject.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2));
+ }
+
if (testObject.RecursiveObject != null)
{
target.RecursiveObject = MapToDtoExt(testObject.RecursiveObject);
@@ -245,6 +255,11 @@ public static partial int ParseableInt(string value)
target.NestedNullable = MapToTestObjectNested(dto.NestedNullable);
}
+ if (dto.TupleValue != null)
+ {
+ target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString());
+ }
+
if (dto.RecursiveObject != null)
{
target.RecursiveObject = MapFromDto(dto.RecursiveObject);
@@ -333,6 +348,11 @@ public static partial void UpdateDto(global::Riok.Mapperly.IntegrationTests.Mode
target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable;
}
+ if (source.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2));
+ }
+
if (source.RecursiveObject != null)
{
target.RecursiveObject = MapToDtoExt(source.RecursiveObject);
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.RunMappingShouldWork.verified.txt
index 90335d9b16..6fa816fe88 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.RunMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.RunMappingShouldWork.verified.txt
@@ -19,6 +19,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs
index df5b5c8a04..a6c9b1f7c0 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs
@@ -33,6 +33,11 @@ public static partial class DeepCloningMapper
target.NestedNullableTargetNotNullable = MapToTestObjectNested(src.NestedNullableTargetNotNullable);
}
+ if (src.TupleValue != null)
+ {
+ target.TupleValue = (A: src.TupleValue.Value.A, src.TupleValue.Value.Item2);
+ }
+
if (src.RecursiveObject != null)
{
target.RecursiveObject = Copy(src.RecursiveObject);
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.RunMappingShouldWork.verified.txt
index 4d03e0e639..9c57b7ac72 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.RunMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.RunMappingShouldWork.verified.txt
@@ -20,6 +20,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.SnapshotGeneratedSource.verified.cs
index 6045c2198d..5e2b66e277 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.SnapshotGeneratedSource.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.SnapshotGeneratedSource.verified.cs
@@ -78,6 +78,11 @@ public partial int ParseableInt(string value)
target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable;
}
+ if (testObject.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2));
+ }
+
if (testObject.RecursiveObject != null)
{
target.RecursiveObject = MapToDto(testObject.RecursiveObject);
@@ -159,6 +164,11 @@ public partial int ParseableInt(string value)
target.NestedNullable = MapToTestObjectNested(dto.NestedNullable);
}
+ if (dto.TupleValue != null)
+ {
+ target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString());
+ }
+
if (dto.RecursiveObject != null)
{
target.RecursiveObject = MapFromDto(dto.RecursiveObject);
@@ -247,6 +257,11 @@ public partial void UpdateDto(global::Riok.Mapperly.IntegrationTests.Models.Test
target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable;
}
+ if (source.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2));
+ }
+
if (source.RecursiveObject != null)
{
target.RecursiveObject = MapToDto(source.RecursiveObject);
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt
index d0a7271d51..3437d7e928 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt
@@ -15,6 +15,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunMappingShouldWork.verified.txt
index 5ade304416..06c3a7cec3 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunMappingShouldWork.verified.txt
@@ -20,6 +20,10 @@
},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: fooBar3,
+ TupleValue: {
+ Item1: 10,
+ Item2: 20
+ },
RecursiveObject: {
CtorValue: 5,
CtorValue2: 100,
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs
index 699997e00c..c79d3b0e7c 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs
@@ -77,6 +77,11 @@ public static partial int ParseableInt(string value)
target.StringNullableTargetNotNullable = src.StringNullableTargetNotNullable;
}
+ if (src.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(src.TupleValue.Value.A), ParseableInt(src.TupleValue.Value.Item2));
+ }
+
if (src.RecursiveObject != null)
{
target.RecursiveObject = MapToDtoExt(src.RecursiveObject);
@@ -173,6 +178,11 @@ public static partial int ParseableInt(string value)
target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable;
}
+ if (testObject.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2));
+ }
+
if (testObject.RecursiveObject != null)
{
target.RecursiveObject = MapToDtoExt(testObject.RecursiveObject);
@@ -254,6 +264,11 @@ public static partial int ParseableInt(string value)
target.NestedNullable = MapToTestObjectNested(dto.NestedNullable);
}
+ if (dto.TupleValue != null)
+ {
+ target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString());
+ }
+
if (dto.RecursiveObject != null)
{
target.RecursiveObject = MapFromDto(dto.RecursiveObject);
@@ -342,6 +357,11 @@ public static partial void UpdateDto(global::Riok.Mapperly.IntegrationTests.Mode
target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable;
}
+ if (source.TupleValue != null)
+ {
+ target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2));
+ }
+
if (source.RecursiveObject != null)
{
target.RecursiveObject = MapToDtoExt(source.RecursiveObject);
diff --git a/test/Riok.Mapperly.Tests/Mapping/ValueTupleTest.cs b/test/Riok.Mapperly.Tests/Mapping/ValueTupleTest.cs
new file mode 100644
index 0000000000..db888bbd63
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/Mapping/ValueTupleTest.cs
@@ -0,0 +1,541 @@
+using Riok.Mapperly.Abstractions;
+using Riok.Mapperly.Diagnostics;
+
+namespace Riok.Mapperly.Tests.Mapping;
+
+[UsesVerify]
+public class ValueTupleTest
+{
+ [Fact]
+ public void TupleToTuple()
+ {
+ var source = TestSourceBuilder.Mapping("(int, string)", "(int, string)");
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return source;");
+ }
+
+ [Fact]
+ public void TupleToTupleWithDeepCloning()
+ {
+ var source = TestSourceBuilder.Mapping("(int, string)", "(int, string)", TestSourceBuilderOptions.WithDeepCloning);
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.Item1, source.Item2);");
+ }
+
+ [Fact]
+ public void TupleToDifferentTypeTuple()
+ {
+ var source = TestSourceBuilder.Mapping("(int, string)", "(long, int)");
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return ((long)source.Item1, int.Parse(source.Item2));");
+ }
+
+ [Fact]
+ public void NamedTupleToTuple()
+ {
+ var source = TestSourceBuilder.Mapping("(int A, string B)", "(int, string)");
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.A, source.B);");
+ }
+
+ [Fact]
+ public void TupleToNamedTuple()
+ {
+ var source = TestSourceBuilder.Mapping("(int, string)", "(int A, string B)");
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (A: source.Item1, B: source.Item2);");
+ }
+
+ [Fact]
+ public void NamedTupleToNamedTuple()
+ {
+ var source = TestSourceBuilder.Mapping("(int A, string B)", "(int A, string B)");
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return source;");
+ }
+
+ [Fact]
+ public void NamedTupleToNamedTupleWithDeepCloning()
+ {
+ var source = TestSourceBuilder.Mapping("(int A, string B)", "(int A, string B)", TestSourceBuilderOptions.WithDeepCloning);
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (A: source.A, B: source.B);");
+ }
+
+ [Fact]
+ public void NamedTupleToNamedTupleWithPositionalResolve()
+ {
+ var source = TestSourceBuilder.Mapping("(int A, string B)", "(int C, string D)", TestSourceBuilderOptions.WithDeepCloning);
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (C: source.A, D: source.B);");
+ }
+
+ [Fact]
+ public void PartiallyNamedTupleToPartiallyNamedTuple()
+ {
+ var source = TestSourceBuilder.Mapping("(int, string A)", "(int A, string)");
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveSingleMethodBody("return (A: int.Parse(source.A), source.A);");
+ }
+
+ [Fact]
+ public void TupleToTupleNamedItems()
+ {
+ var source = TestSourceBuilder.Mapping("(int, string)", "(int Item2, string Item1)");
+
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveSingleMethodBody("return (Item2: int.Parse(source.Item2), Item1: source.Item1.ToString());");
+ }
+
+ [Fact]
+ public void TupleNamedItemsToTuple()
+ {
+ var source = TestSourceBuilder.Mapping("(int Item2, string Item1)", "(int, string)");
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (int.Parse(source.Item1), source.Item2.ToString());");
+ }
+
+ [Fact]
+ public void TupleToClassShouldNotDiagnosticUnmapped()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "(string, int)",
+ "A",
+ "class A { public string Item1 { get; set; } public int Item2 { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::A();
+ target.Item1 = source.Item1;
+ target.Item2 = source.Item2;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void NamedTupleToClassShouldNotDiagnosticUnmapped()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "(string A, int B)",
+ "C",
+ "class C { public string A { get; set; } public int B { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::C();
+ target.A = source.A;
+ target.B = source.B;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void ClassToTuple()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "(int B, string C)",
+ "public class A { public int B { get;set;} public int C {get;set;} }"
+ );
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (B: source.B, C: source.C.ToString());");
+ }
+
+ [Fact]
+ public void TupleToTupleWithIgnoredSource()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapperIgnoreSource("C")]
+ partial (int, string) Map((int A, string B, int C) source);
+ """
+ );
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.A, source.B);");
+ }
+
+ [Fact]
+ public void TupleToTupleWithIgnoredSourceByPosition()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapperIgnoreSource("Item3")]
+ partial (int, string) Map((int A, string B, int) source);
+ """
+ );
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.A, source.B);");
+ }
+
+ [Fact]
+ public void ClassToTupleWithIgnoredSource()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapperIgnoreSource("A")]
+ partial (int, int) Map(B source);
+ """,
+ "public class B { public int Item1 { get;set;} public int A {get;set;} public int Item2 {get;set;} }"
+ );
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.Item1, source.Item2);");
+ }
+
+ [Fact]
+ public void IgnoredNamedSourceWithPositionalShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapperIgnoreSource("Item3")]
+ partial (int, string) Map((int A, string B, int C) source);
+ """
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.IgnoredSourceMemberNotFound)
+ .HaveSingleMethodBody("return (source.A, source.B);");
+ }
+
+ [Fact]
+ public void TupleWithNonExistentIgnoreSourceShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapperIgnoreSource("D")]
+ partial (int, string) Map((int A, string B) source);
+ """
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.IgnoredSourceMemberNotFound)
+ .HaveSingleMethodBody("return (source.A, source.B);");
+ }
+
+ [Fact]
+ public void InvalidTupleShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapperIgnoreSource("A")]
+ partial (int, int) Map((int, int A) source);
+ """
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound)
+ .HaveDiagnostic(DiagnosticDescriptors.NoConstructorFound)
+ .HaveSingleMethodBody(
+ """
+ // Could not generate mapping
+ throw new System.NotImplementedException();
+ """
+ );
+ }
+
+ [Fact]
+ public void IgnoreTargetTuple()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapperIgnoreTarget("A")]
+ partial (int, int A) Map((string, int A) source);
+ """
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.NoConstructorFound)
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ // Could not generate mapping
+ throw new System.NotImplementedException();
+ """
+ );
+ }
+
+ [Fact]
+ public void IgnoreTargetByPosition()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapperIgnoreTarget("Item1")]
+ partial (int, int A) Map((string, int A) source);
+ """
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.NoConstructorFound)
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ // Could not generate mapping
+ throw new System.NotImplementedException();
+ """
+ );
+ }
+
+ [Fact]
+ public void IgnoreTargetWithNonExistentTargetShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapperIgnoreTarget("B")]
+ partial (int, int A) Map((string, int) source);
+ """
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.IgnoredTargetMemberNotFound)
+ .HaveSingleMethodBody("return (int.Parse(source.Item1), A: source.Item2);");
+ }
+
+ [Fact]
+ public void IgnoreTargetWithNonExistentPositionalShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapperIgnoreTarget("Item3")]
+ partial (int, int A) Map((string, int) source);
+ """
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.IgnoredTargetMemberNotFound)
+ .HaveSingleMethodBody("return (int.Parse(source.Item1), A: source.Item2);");
+ }
+
+ [Fact]
+ public void TupleToTupleWithMapProperty()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("C", "A")]
+ [MapProperty("Item3", "Item2")]
+ partial (int A, int) Map((int B, int C, int) source);
+ """
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveSingleMethodBody("return (A: source.C, source.Item3);");
+ }
+
+ [Fact]
+ public Task TuplePropertyToTupleProperty()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ "class A { public (int A, int) Value { get; set; } }",
+ "class B { public (string A, int) Value { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task MapPropertyShouldMapToTupleField()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("Item2", "Item1.Value")]
+ partial (A, int) Map((B, string) source);
+ """,
+ "class A { public int Value { get; set; } }",
+ "class B { public int Value { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task MapPropertyShouldMapNestedTuple()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("Item2", "Item1.Item1")]
+ partial ((int, int), int) Map(((int, int), string) source);
+ """
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task MapPropertyShouldMapNamedNestedTuple()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("Item1", "C")]
+ [MapProperty("B", "C.D")]
+ partial ((int, int D) C, int) Map(((int F, int G), string B) source);
+ """
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task MapPropertyShouldMapFieldAndNestedTuple()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("A", "F")]
+ [MapProperty("D", "E")]
+ [MapProperty("D", "F.H")]
+ partial (string E, (long G, int H) F) Map(((int B, int C) A, string D) source);
+ """
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public void TupleToValueTuple()
+ {
+ var source = TestSourceBuilder.Mapping("(int A, string B)", "ValueTuple");
+
+ TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.A, source.B);");
+ }
+
+ [Fact]
+ public void QueryableTupleToQueryableTuple()
+ {
+ var source = TestSourceBuilder.Mapping("System.Linq.IQueryable<(int A, string B)>", "System.Linq.IQueryable<(int, string)>");
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ #nullable disable
+ return System.Linq.Queryable.Select(source, x => new global::System.ValueTuple(x.A, x.B));
+ #nullable enable
+ """
+ );
+ }
+
+ [Fact]
+ public void QueryableTupleToIQueryableValueTuple()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "System.Linq.IQueryable<(int A, string B)>",
+ "System.Linq.IQueryable>"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ #nullable disable
+ return System.Linq.Queryable.Select(source, x => new global::System.ValueTuple(x.A, x.B));
+ #nullable enable
+ """
+ );
+ }
+
+ [Fact]
+ public void TupleToTupleWithManyMapPropertyShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("B", "A")]
+ [MapProperty("B", "A")]
+ partial (int A, int) Map((int, string B) source);
+ """
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.MultipleConfigurationsForConstructorParameter);
+ }
+
+ [Fact]
+ public void TupleToTupleWithMapPropertyWithImplicitNameShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("Item2", "Item1")]
+ partial (int A, int B) Map((int C, int D) source);
+ """
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.ConfiguredMappingTargetMemberNotFound);
+ }
+
+ [Fact]
+ public void TupleMappingDisabledShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "(int, string)",
+ "(string, int)",
+ TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.Tuple)
+ );
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.CouldNotCreateMapping)
+ .HaveSingleMethodBody(
+ """
+ // Could not generate mapping
+ throw new System.NotImplementedException();
+ """
+ );
+ }
+
+ [Fact]
+ public void ClassToTupleWithNoMappingsShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.Mapping("A", "(int, int)", "public class A { }");
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound)
+ .HaveSingleMethodBody(
+ """
+ // Could not generate mapping
+ throw new System.NotImplementedException();
+ """
+ );
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapFieldAndNestedTuple#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapFieldAndNestedTuple#Mapper.g.verified.cs
new file mode 100644
index 0000000000..9b8a655b42
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapFieldAndNestedTuple#Mapper.g.verified.cs
@@ -0,0 +1,12 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial (string E, (long G, int H) F) Map(((int B, int C) A, string D) source)
+ {
+ var target = (E: source.D, F: (G: (long)source.A.B, H: source.A.C));
+ target.F.H = int.Parse(source.D);
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapNamedNestedTuple#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapNamedNestedTuple#Mapper.g.verified.cs
new file mode 100644
index 0000000000..5f8f2d3e76
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapNamedNestedTuple#Mapper.g.verified.cs
@@ -0,0 +1,12 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial ((int, int D) C, int) Map(((int F, int G), string B) source)
+ {
+ var target = (C: (source.Item1.F, D: source.Item1.G), int.Parse(source.B));
+ target.C.D = int.Parse(source.B);
+ return target;
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapNestedTuple#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapNestedTuple#Mapper.g.verified.cs
new file mode 100644
index 0000000000..d8432bf55c
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapNestedTuple#Mapper.g.verified.cs
@@ -0,0 +1,12 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial ((int, int), int) Map(((int, int), string) source)
+ {
+ var target = (source.Item1, int.Parse(source.Item2));
+ target.Item1.Item1 = int.Parse(source.Item2);
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapToTupleField#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapToTupleField#Mapper.g.verified.cs
new file mode 100644
index 0000000000..ced7e60ab6
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapToTupleField#Mapper.g.verified.cs
@@ -0,0 +1,19 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial (global::A, int) Map((global::B, string) source)
+ {
+ var target = (MapToA(source.Item1), int.Parse(source.Item2));
+ target.Item1.Value = int.Parse(source.Item2);
+ return target;
+ }
+
+ private global::A MapToA(global::B source)
+ {
+ var target = new global::A();
+ target.Value = source.Value;
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.TuplePropertyToTupleProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.TuplePropertyToTupleProperty#Mapper.g.verified.cs
new file mode 100644
index 0000000000..c791967bf4
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.TuplePropertyToTupleProperty#Mapper.g.verified.cs
@@ -0,0 +1,12 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial global::B Map(global::A source)
+ {
+ var target = new global::B();
+ target.Value = (A: source.Value.A.ToString(), source.Value.Item2);
+ return target;
+ }
+}
\ No newline at end of file