diff --git a/docs/docs/configuration/derived-type-mapping.md b/docs/docs/configuration/derived-type-mapping.md
index eba9b5e72f..b15aa0da2b 100644
--- a/docs/docs/configuration/derived-type-mapping.md
+++ b/docs/docs/configuration/derived-type-mapping.md
@@ -8,7 +8,7 @@ description: Map derived types and interfaces
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
-Mapperly supports interfaces and base types as mapping sources and targets, for both new instance and [existing target](./existing-target.md) mappings.
+Mapperly supports interfaces and base types as mapping sources and targets, for both new instance and [existing target](./existing-target.mdx) mappings.
To do this, Mapperly needs to know which derived types exist.
This can be configured with the `MapDerivedTypeAttribute`:
diff --git a/docs/docs/configuration/existing-target.md b/docs/docs/configuration/existing-target.md
deleted file mode 100644
index 5919152691..0000000000
--- a/docs/docs/configuration/existing-target.md
+++ /dev/null
@@ -1,76 +0,0 @@
----
-sidebar_position: 9
-description: Map to an existing target object
----
-
-# Existing target object
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-If an existing object instance should be used as target, you can define the mapping method as void with the target as second parameter:
-
-```csharp title="Mapper declaration"
-[Mapper]
-public partial class CarMapper
-{
- // highlight-start
- public partial void CarToCarDto(Car car, CarDto dto);
- // highlight-end
-}
-```
-
-```csharp title="Mapper usage"
-var mapper = new CarMapper();
-var car = new Car { NumberOfSeats = 10, ... };
-var dto = new CarDto();
-
-mapper.CarToCarDto(car, dto);
-dto.NumberOfSeats.Should().Be(10);
-```
-
-## Merge objects
-
-To merge two objects together, `AllowNullPropertyAssignment` can be set to `false`.
-This ignores all properties on the source with a `null` value.
-
-
-
-
-```csharp
-// highlight-start
-[Mapper(AllowNullPropertyAssignment = false)]
-// highlight-end
-static partial class FruitMapper
-{
- public static partial void ApplyUpdate(FruitUpdate update, Fruit fruit);
-}
-
-class Fruit { public required string Name { get; set; } public required string Color { get; set; } }
-record FruitUpdate(string? Name, string? Color);
-```
-
-
-
-
-```csharp
-static partial class FruitMapper
-{
- public static partial void Update(global::FruitUpdate update, global::Fruit fruit)
- {
- if (update.Name != null)
- {
- fruit.Name = update.Name;
- }
- if (update.Color != null)
- {
- fruit.Color = update.Color;
- }
- }
-}
-```
-
-
-
-
-See also [null value handling](./mapper.mdx#null-values).
diff --git a/docs/docs/configuration/existing-target.mdx b/docs/docs/configuration/existing-target.mdx
new file mode 100644
index 0000000000..27f6f5697c
--- /dev/null
+++ b/docs/docs/configuration/existing-target.mdx
@@ -0,0 +1,114 @@
+---
+sidebar_position: 9
+description: Map to an existing target object
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Existing target object
+
+If an existing object instance should be used as target, you can define the mapping method as void with the target as second parameter:
+
+
+
+ ```csharp
+ [Mapper]
+ public partial class CarMapper
+ {
+ // highlight-start
+ public partial void UpdateCarDto(Car car, CarDto dto);
+ // highlight-end
+ }
+ ```
+
+
+ ```csharp
+ var mapper = new CarMapper();
+ var car = new Car { NumberOfSeats = 10, ... };
+ var dto = new CarDto();
+
+ mapper.UpdateCarDto(car, dto);
+ dto.NumberOfSeats.Should().Be(10);
+ ```
+
+
+
+## Merge objects
+
+To merge two objects together, `AllowNullPropertyAssignment` can be set to `false`.
+This ignores all properties on the source with a `null` value.
+
+
+
+ ```csharp
+ // highlight-start
+ [Mapper(AllowNullPropertyAssignment = false)]
+ // highlight-end
+ static partial class FruitMapper
+ {
+ // highlight-start
+ public static partial void ApplyUpdate(FruitUpdate update, Fruit fruit);
+ // highlight-end
+ }
+
+ class Fruit { public required string Name { get; set; } public required string Color { get; set; } }
+ record FruitUpdate(string? Name, string? Color);
+ ```
+
+
+ ```csharp
+ FruitMapper.ApplyUpdate(myUpdateRequest, myFruit);
+ ```
+
+
+ ```csharp
+ static partial class FruitMapper
+ {
+ public static partial void Update(global::FruitUpdate update, global::Fruit fruit)
+ {
+ if (update.Name != null)
+ {
+ fruit.Name = update.Name;
+ }
+ if (update.Color != null)
+ {
+ fruit.Color = update.Color;
+ }
+ }
+ }
+ ```
+
+
+
+See also [null value handling](./mapper.mdx#null-values).
+
+The `MappingTarget` attribute allows setting the first method parameter as mapping target:
+
+
+
+
+ ```csharp
+ // highlight-start
+ [Mapper(AllowNullPropertyAssignment = false)]
+ // highlight-end
+ static partial class FruitMapper
+ {
+ // highlight-start
+ public static partial void ApplyUpdate([MappingTarget] this Fruit fruit, FruitUpdate update);
+ // highlight-end
+ }
+
+ class Fruit { public required string Name { get; set; } public required string Color { get; set; } }
+ record FruitUpdate(string? Name, string? Color);
+ ```
+
+
+
+ ```csharp
+ myFruit.ApplyUpdate(myUpdateRequest);
+ ```
+
+
+
+See also [extension methods](./static-mappers.md).
diff --git a/docs/docs/configuration/static-mappers.md b/docs/docs/configuration/static-mappers.md
index c49be0ba33..fa7ce830cc 100644
--- a/docs/docs/configuration/static-mappers.md
+++ b/docs/docs/configuration/static-mappers.md
@@ -11,7 +11,7 @@ Mapperly supports static mappers and extension methods:
[Mapper]
public static partial class CarMapper
{
- public static partial CarDto CarToCarDto(this Car car);
+ public static partial CarDto ToCarDto(this Car car);
private static int TimeSpanToHours(TimeSpan t) => t.Hours;
}
diff --git a/src/Riok.Mapperly.Abstractions/MappingTargetAttribute.cs b/src/Riok.Mapperly.Abstractions/MappingTargetAttribute.cs
new file mode 100644
index 0000000000..97986a2bf5
--- /dev/null
+++ b/src/Riok.Mapperly.Abstractions/MappingTargetAttribute.cs
@@ -0,0 +1,10 @@
+using System.Diagnostics;
+
+namespace Riok.Mapperly.Abstractions;
+
+///
+/// Marks a given parameter as the mapping target.
+///
+[AttributeUsage(AttributeTargets.Parameter)]
+[Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")]
+public sealed class MappingTargetAttribute : Attribute;
diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
index 467798939c..3b12015a40 100644
--- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
+++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
@@ -182,3 +182,7 @@ Riok.Mapperly.Abstractions.MapValueAttribute.MapValueAttribute(string![]! target
Riok.Mapperly.Abstractions.MapValueAttribute.Target.get -> System.Collections.Generic.IReadOnlyCollection!
Riok.Mapperly.Abstractions.MapValueAttribute.TargetFullName.get -> string!
Riok.Mapperly.Abstractions.MapValueAttribute.Value.get -> object?
+Riok.Mapperly.Abstractions.MappingTargetAttribute
+Riok.Mapperly.Abstractions.MappingTargetAttribute.MappingTargetAttribute() -> void
+Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string! source, string![]! target) -> void
+Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string![]! source, string! target) -> void
diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt
index 41a6d8564b..7dc5c58110 100644
--- a/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt
+++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt
@@ -1,3 +1 @@
#nullable enable
-Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string! source, string![]! target) -> void
-Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string![]! source, string! target) -> void
diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs
index 6f3c549322..07799c5a2a 100644
--- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs
+++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs
@@ -73,10 +73,7 @@ public bool CanAssign(ITypeSymbol sourceType, ITypeSymbol targetType)
&& (targetType.IsNullable() || !sourceType.IsNullable());
}
- public MethodParameter? WrapOptionalMethodParameter(IParameterSymbol? symbol)
- {
- return symbol == null ? null : WrapMethodParameter(symbol);
- }
+ public MethodParameter? WrapOptionalMethodParameter(IParameterSymbol? symbol) => symbol == null ? null : WrapMethodParameter(symbol);
public MethodParameter WrapMethodParameter(IParameterSymbol symbol) => new(symbol, UpgradeNullable(symbol.Type));
diff --git a/src/Riok.Mapperly/Descriptors/UserMappingMethodParameterExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMappingMethodParameterExtractor.cs
new file mode 100644
index 0000000000..96d33d9ff5
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/UserMappingMethodParameterExtractor.cs
@@ -0,0 +1,179 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.CodeAnalysis;
+using Riok.Mapperly.Abstractions;
+using Riok.Mapperly.Abstractions.ReferenceHandling;
+using Riok.Mapperly.Diagnostics;
+using Riok.Mapperly.Symbols;
+
+namespace Riok.Mapperly.Descriptors;
+
+internal static class UserMappingMethodParameterExtractor
+{
+ public static bool BuildParameters(
+ SimpleMappingBuilderContext ctx,
+ IMethodSymbol method,
+ [NotNullWhen(true)] out MappingMethodParameters? parameters
+ )
+ {
+ // the source param is always required
+ var expectedParameterCount = 1;
+
+ var refHandlerParameter = FindReferenceHandlerParameter(ctx, method);
+ if (refHandlerParameter.HasValue)
+ {
+ expectedParameterCount++;
+ }
+
+ var sourceParameter = FindSourceParameter(ctx, method, refHandlerParameter);
+ if (!sourceParameter.HasValue)
+ {
+ parameters = null;
+ return false;
+ }
+
+ var targetParameter = FindTargetParameter(ctx, method, sourceParameter.Value, refHandlerParameter);
+
+ // If the method returns void, a target parameter is required
+ // if the method doesn't return void, a target parameter is not allowed.
+ if (method.ReturnsVoid == !targetParameter.HasValue)
+ {
+ parameters = null;
+ return false;
+ }
+
+ if (targetParameter.HasValue)
+ {
+ expectedParameterCount++;
+ }
+
+ parameters = new MappingMethodParameters(sourceParameter.Value, targetParameter, refHandlerParameter);
+ return method.Parameters.Length == expectedParameterCount;
+ }
+
+ public static bool BuildRuntimeTargetTypeMappingParameters(
+ SimpleMappingBuilderContext ctx,
+ IMethodSymbol method,
+ [NotNullWhen(true)] out RuntimeTargetTypeMappingMethodParameters? parameters
+ )
+ {
+ // existing target instance runtime typed mappings are not supported
+ if (method.ReturnsVoid)
+ {
+ parameters = null;
+ return false;
+ }
+
+ // the source and target type param is always required
+ var expectedParameterCount = 2;
+
+ var refHandlerParameter = FindReferenceHandlerParameter(ctx, method);
+ if (refHandlerParameter.HasValue)
+ {
+ expectedParameterCount++;
+ }
+
+ var sourceParameter = FindSourceParameter(ctx, method, refHandlerParameter);
+ if (!sourceParameter.HasValue)
+ {
+ parameters = null;
+ return false;
+ }
+
+ // the target type param needs to exist
+ // and needs to be of type System.Type
+ var targetTypeParameter = FindTargetParameter(ctx, method, sourceParameter.Value, refHandlerParameter);
+ if (!targetTypeParameter.HasValue || !SymbolEqualityComparer.Default.Equals(targetTypeParameter.Value.Type, ctx.Types.Get()))
+ {
+ parameters = null;
+ return false;
+ }
+
+ if (method.Parameters.Length != expectedParameterCount)
+ {
+ parameters = null;
+ return false;
+ }
+
+ parameters = new RuntimeTargetTypeMappingMethodParameters(sourceParameter.Value, targetTypeParameter.Value, refHandlerParameter);
+ return true;
+ }
+
+ private static MethodParameter? FindSourceParameter(
+ SimpleMappingBuilderContext ctx,
+ IMethodSymbol method,
+ MethodParameter? refHandlerParameter
+ )
+ {
+ var refHandlerParameterOrdinal = refHandlerParameter?.Ordinal ?? -1;
+
+ // source parameter is the first parameter not annotated as reference handler or mapping target
+ var sourceParameterSymbol = method.Parameters.FirstOrDefault(p =>
+ p.Ordinal != refHandlerParameterOrdinal
+ && !ctx.SymbolAccessor.HasAttribute(p)
+ && !ctx.SymbolAccessor.HasAttribute(p)
+ );
+ return ctx.SymbolAccessor.WrapOptionalMethodParameter(sourceParameterSymbol);
+ }
+
+ private static MethodParameter? FindTargetParameter(
+ SimpleMappingBuilderContext ctx,
+ IMethodSymbol method,
+ MethodParameter sourceParameter,
+ MethodParameter? refHandlerParameter
+ )
+ {
+ var refHandlerParameterOrdinal = refHandlerParameter?.Ordinal ?? -1;
+
+ // The target parameter is the first parameter,
+ // which is not the source parameter,
+ // and is not annotated as reference handling parameter.
+ // It may be annotated as mapping target
+ // (for example, if it is the very first parameter, which is often the case in extension methods).
+ var targetParameterSymbol = method.Parameters.FirstOrDefault(p =>
+ p.Ordinal != sourceParameter.Ordinal
+ && p.Ordinal != refHandlerParameterOrdinal
+ && !ctx.SymbolAccessor.HasAttribute(p)
+ );
+ return ctx.SymbolAccessor.WrapOptionalMethodParameter(targetParameterSymbol);
+ }
+
+ private static MethodParameter? FindReferenceHandlerParameter(SimpleMappingBuilderContext ctx, IMethodSymbol method)
+ {
+ var refHandlerParameterSymbol = method.Parameters.FirstOrDefault(p =>
+ ctx.SymbolAccessor.HasAttribute(p)
+ );
+ if (refHandlerParameterSymbol == null)
+ return null;
+
+ // the reference handler parameter cannot also be the target parameter
+ if (ctx.SymbolAccessor.HasAttribute(refHandlerParameterSymbol))
+ {
+ ctx.ReportDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, method, method.Name);
+ }
+
+ var refHandlerParameter = ctx.SymbolAccessor.WrapMethodParameter(refHandlerParameterSymbol);
+ if (!SymbolEqualityComparer.Default.Equals(ctx.Types.Get(), refHandlerParameter.Type))
+ {
+ ctx.ReportDiagnostic(
+ DiagnosticDescriptors.ReferenceHandlerParameterWrongType,
+ refHandlerParameterSymbol,
+ method.ContainingType.ToDisplayString(),
+ method.Name,
+ ctx.Types.Get().ToDisplayString(),
+ refHandlerParameterSymbol.Type.ToDisplayString()
+ );
+ }
+
+ if (!ctx.Configuration.Mapper.UseReferenceHandling)
+ {
+ ctx.ReportDiagnostic(
+ DiagnosticDescriptors.ReferenceHandlingNotEnabled,
+ refHandlerParameterSymbol,
+ method.ContainingType.ToDisplayString(),
+ method.Name
+ );
+ }
+
+ return refHandlerParameter;
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
index 2756eb230b..1447216dc5 100644
--- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
+++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
@@ -1,8 +1,6 @@
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
-using Riok.Mapperly.Abstractions.ReferenceHandling;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.UserMappings;
@@ -21,7 +19,7 @@ internal static IEnumerable ExtractUserMappings(SimpleMappingBuild
foreach (var method in methods)
{
var mapping =
- BuilderUserDefinedMapping(ctx, method)
+ BuildUserDefinedMapping(ctx, method)
?? BuildUserImplementedMapping(
ctx,
method,
@@ -109,7 +107,7 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM
{
requireAttribute &= !ctx.Configuration.Mapper.AutoUserMappings;
- // ignore all non ordinary methods (eg. ctor, operators, etc.) and methods declared on the object type (eg. ToString)
+ // ignore all non-ordinary methods (e.g. ctor, operators, etc.) and methods declared on the object type (e.g. ToString)
return method.MethodKind == MethodKind.Ordinary
&& ctx.SymbolAccessor.IsDirectlyAccessible(method)
&& !SymbolEqualityComparer.Default.Equals(method.ReceiverType, ctx.Compilation.ObjectType)
@@ -133,7 +131,7 @@ bool isExternal
var userMappingConfig = GetUserMappingConfig(ctx, method, out var hasAttribute);
var valid = !method.IsGenericMethod && (allowPartial || !method.IsPartialDefinition) && (!isStatic || method.IsStatic);
- if (!valid || !BuildParameters(ctx, method, out var parameters))
+ if (!valid || !UserMappingMethodParameterExtractor.BuildParameters(ctx, method, out var parameters))
{
if (hasAttribute)
{
@@ -171,7 +169,7 @@ bool isExternal
);
}
- private static IUserMapping? BuilderUserDefinedMapping(SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol)
+ private static IUserMapping? BuildUserDefinedMapping(SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol)
{
if (!methodSymbol.IsPartialDefinition)
return null;
@@ -182,7 +180,14 @@ bool isExternal
return null;
}
- if (!methodSymbol.IsGenericMethod && BuildRuntimeTargetTypeMappingParameters(ctx, methodSymbol, out var runtimeTargetTypeParams))
+ if (
+ !methodSymbol.IsGenericMethod
+ && UserMappingMethodParameterExtractor.BuildRuntimeTargetTypeMappingParameters(
+ ctx,
+ methodSymbol,
+ out var runtimeTargetTypeParams
+ )
+ )
{
return new UserDefinedNewInstanceRuntimeTargetTypeParameterMapping(
methodSymbol,
@@ -194,7 +199,7 @@ bool isExternal
);
}
- if (!BuildParameters(ctx, methodSymbol, out var parameters))
+ if (!UserMappingMethodParameterExtractor.BuildParameters(ctx, methodSymbol, out var parameters))
{
ctx.ReportDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, methodSymbol, methodSymbol.Name);
return null;
@@ -234,146 +239,6 @@ bool isExternal
);
}
- private static bool BuildRuntimeTargetTypeMappingParameters(
- SimpleMappingBuilderContext ctx,
- IMethodSymbol method,
- [NotNullWhen(true)] out RuntimeTargetTypeMappingMethodParameters? parameters
- )
- {
- var expectedParametersCount = 0;
-
- // reference handler parameter is always annotated
- var refHandlerParameter = BuildReferenceHandlerParameter(ctx, method);
- var refHandlerParameterOrdinal = refHandlerParameter?.Ordinal ?? -1;
- if (refHandlerParameter.HasValue)
- {
- expectedParametersCount++;
- }
-
- // source parameter is the first parameter (except if the reference handler is the first parameter)
- var sourceParameterSymbol = method.Parameters.FirstOrDefault(p => p.Ordinal != refHandlerParameterOrdinal);
- if (sourceParameterSymbol == null)
- {
- parameters = null;
- return false;
- }
-
- var sourceParameter = ctx.SymbolAccessor.WrapMethodParameter(sourceParameterSymbol);
- expectedParametersCount++;
-
- // target type parameter is the second parameter (except if the reference handler is the first or the second parameter)
- var targetTypeParameterSymbol = method.Parameters.FirstOrDefault(p =>
- p.Ordinal != sourceParameter.Ordinal && p.Ordinal != refHandlerParameterOrdinal
- );
- if (
- targetTypeParameterSymbol == null
- || !SymbolEqualityComparer.Default.Equals(targetTypeParameterSymbol.Type, ctx.Types.Get())
- )
- {
- parameters = null;
- return false;
- }
-
- var targetTypeParameter = ctx.SymbolAccessor.WrapMethodParameter(targetTypeParameterSymbol);
- expectedParametersCount++;
-
- if (method.Parameters.Length != expectedParametersCount)
- {
- parameters = null;
- return false;
- }
-
- parameters = new RuntimeTargetTypeMappingMethodParameters(sourceParameter, targetTypeParameter, refHandlerParameter);
- return true;
- }
-
- private static bool BuildParameters(
- SimpleMappingBuilderContext ctx,
- IMethodSymbol method,
- [NotNullWhen(true)] out MappingMethodParameters? parameters
- )
- {
- var expectedParameterCount = 1;
-
- // reference handler parameter is always annotated
- var refHandlerParameter = BuildReferenceHandlerParameter(ctx, method);
- var refHandlerParameterOrdinal = refHandlerParameter?.Ordinal ?? -1;
- if (refHandlerParameter.HasValue)
- {
- expectedParameterCount++;
- }
-
- // source parameter is the first parameter (except if the reference handler is the first parameter)
- var sourceParameterSymbol = method.Parameters.FirstOrDefault(p => p.Ordinal != refHandlerParameterOrdinal);
- if (sourceParameterSymbol == null)
- {
- parameters = null;
- return false;
- }
-
- var sourceParameter = ctx.SymbolAccessor.WrapMethodParameter(sourceParameterSymbol);
-
- // target parameter is the second parameter (except if the reference handler is the first or the second parameter)
- // if the method returns void, a target parameter is required
- // if the method doesnt return void, a target parameter is not allowed
- var targetParameter = ctx.SymbolAccessor.WrapOptionalMethodParameter(
- method.Parameters.FirstOrDefault(p => p.Ordinal != sourceParameter.Ordinal && p.Ordinal != refHandlerParameterOrdinal)
- );
- if (method.ReturnsVoid == !targetParameter.HasValue)
- {
- parameters = null;
- return false;
- }
-
- if (targetParameter.HasValue)
- {
- expectedParameterCount++;
- }
-
- if (method.Parameters.Length != expectedParameterCount)
- {
- parameters = null;
- return false;
- }
-
- parameters = new MappingMethodParameters(sourceParameter, targetParameter, refHandlerParameter);
- return true;
- }
-
- private static MethodParameter? BuildReferenceHandlerParameter(SimpleMappingBuilderContext ctx, IMethodSymbol method)
- {
- var refHandlerParameterSymbol = method.Parameters.FirstOrDefault(p =>
- ctx.SymbolAccessor.HasAttribute(p)
- );
- if (refHandlerParameterSymbol == null)
- return null;
-
- var refHandlerParameter = ctx.SymbolAccessor.WrapMethodParameter(refHandlerParameterSymbol);
- if (!SymbolEqualityComparer.Default.Equals(ctx.Types.Get(), refHandlerParameter.Type))
- {
- ctx.ReportDiagnostic(
- DiagnosticDescriptors.ReferenceHandlerParameterWrongType,
- refHandlerParameterSymbol,
- method.ContainingType.ToDisplayString(),
- method.Name,
- ctx.Types.Get().ToDisplayString(),
- refHandlerParameterSymbol.Type.ToDisplayString()
- );
- }
-
- if (!ctx.Configuration.Mapper.UseReferenceHandling)
- {
- ctx.ReportDiagnostic(
- DiagnosticDescriptors.ReferenceHandlingNotEnabled,
- refHandlerParameterSymbol,
- method.ContainingType.ToDisplayString(),
- method.Name
- );
- }
-
- return refHandlerParameter;
- }
-
private static NullFallbackValue? GetTypeSwitchNullArm(IMethodSymbol method, MappingMethodParameters parameters)
{
// target is always the return type for runtime target mappings
diff --git a/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs b/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs
index 5fd2987ee0..97f76dd3c6 100644
--- a/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs
+++ b/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs
@@ -1,6 +1,6 @@
namespace Riok.Mapperly.Symbols;
///
-/// Well known mapping method parameters.
+/// Well-known mapping method parameters.
///
public record MappingMethodParameters(MethodParameter Source, MethodParameter? Target, MethodParameter? ReferenceHandler);
diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs
index 50ab7de392..bdcdc1e4c6 100644
--- a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs
@@ -26,6 +26,10 @@ public static partial class StaticTestMapper
public static partial DateTime DirectDateTime(DateTime dateTime);
+ public static partial void MapIdTargetExt([MappingTarget] this IdObject target, IdObjectDto source);
+
+ public static partial void MapIdTargetFirst([MappingTarget] IdObject target, IdObjectDto source);
+
public static partial IEnumerable MapAllDtos(IEnumerable objects);
[MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))]
diff --git a/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs
index ff585b9ac3..aafa86b96e 100644
--- a/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs
+++ b/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs
@@ -71,5 +71,21 @@ public Task ConstantValuesShouldWork()
var dto = StaticTestMapper.MapConstantValues(obj);
return Verifier.Verify(dto);
}
+
+ [Fact]
+ public void RunMappingIdTargetExtShouldWork()
+ {
+ var model = new IdObject { IdValue = 10 };
+ model.MapIdTargetExt(new IdObjectDto { IdValue = 20 });
+ model.IdValue.Should().Be(20);
+ }
+
+ [Fact]
+ public void RunMappingIdTargetFirstShouldWork()
+ {
+ var model = new IdObject { IdValue = 10 };
+ StaticTestMapper.MapIdTargetFirst(model, new IdObjectDto { IdValue = 20 });
+ model.IdValue.Should().Be(20);
+ }
}
}
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs
index 8866ba0745..434d0b0bb0 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs
@@ -52,6 +52,18 @@ public static partial int ParseableInt(string value)
return dateTime;
}
+ [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+ public static partial void MapIdTargetExt(this global::Riok.Mapperly.IntegrationTests.Models.IdObject target, global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto source)
+ {
+ target.IdValue = DirectInt(source.IdValue);
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+ public static partial void MapIdTargetFirst(global::Riok.Mapperly.IntegrationTests.Models.IdObject target, global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto source)
+ {
+ target.IdValue = DirectInt(source.IdValue);
+ }
+
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
public static partial global::System.Collections.Generic.IEnumerable MapAllDtos(global::System.Collections.Generic.IEnumerable objects)
{
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs
index acd46f8f65..5492effc04 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs
@@ -52,6 +52,18 @@ public static partial int ParseableInt(string value)
return dateTime;
}
+ [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+ public static partial void MapIdTargetExt(this global::Riok.Mapperly.IntegrationTests.Models.IdObject target, global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto source)
+ {
+ target.IdValue = DirectInt(source.IdValue);
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+ public static partial void MapIdTargetFirst(global::Riok.Mapperly.IntegrationTests.Models.IdObject target, global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto source)
+ {
+ target.IdValue = DirectInt(source.IdValue);
+ }
+
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
public static partial global::System.Collections.Generic.IEnumerable MapAllDtos(global::System.Collections.Generic.IEnumerable objects)
{
diff --git a/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs
index 7980e82a12..31478ee5ce 100644
--- a/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs
@@ -15,7 +15,7 @@ public Task ExtensionMapMethodShouldWork()
}
[Fact]
- public Task ExtensionUpdateMethodShouldWork()
+ public Task ExtensionExistingTargetShouldWork()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"static partial void MapToB(this A source, B target);",
@@ -25,4 +25,16 @@ public Task ExtensionUpdateMethodShouldWork()
return TestHelper.VerifyGenerator(source);
}
+
+ [Fact]
+ public Task ExtensionExistingTargetAsFirstParamShouldWork()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "static partial void MapToB([MappingTarget] this B target, A source);",
+ "class A { public int Value { get; set; } }",
+ "class B { public int Value { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
}
diff --git a/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs
index a490ca1c55..dceac3aa85 100644
--- a/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs
@@ -301,4 +301,24 @@ public Task MultipleUserDefinedWithSpecifiedDefault()
return TestHelper.VerifyGenerator(source);
}
+
+ [Fact]
+ public void ReferenceHandlerParameterIsAlsoMappingTargetParameterShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "public partial B Map(A source, [MappingTarget, ReferenceHandler] IReferenceHandler refHandler);",
+ TestSourceBuilderOptions.WithReferenceHandling,
+ "record A;",
+ "record b;"
+ );
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "Map has an unsupported mapping method signature")
+ .HaveDiagnostic(
+ DiagnosticDescriptors.CouldNotCreateMapping,
+ "Could not create mapping from A to B. Consider implementing the mapping manually."
+ )
+ .HaveAssertedAllDiagnostics();
+ }
}
diff --git a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
index 07fb0bf326..6b96416db7 100644
--- a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
@@ -804,4 +804,15 @@ public static D BaseMapping(C source)
);
return TestHelper.VerifyGenerator(source);
}
+
+ [Fact]
+ public void MappingMethodWithSingleTargetParameterShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes("public partial B Map([MappingTarget] A source);", "record A;", "record b;");
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "Map has an unsupported mapping method signature")
+ .HaveAssertedAllDiagnostics();
+ }
}
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionExistingTargetAsFirstParamShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionExistingTargetAsFirstParamShouldWork#Mapper.g.verified.cs
new file mode 100644
index 0000000000..ee490db89a
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionExistingTargetAsFirstParamShouldWork#Mapper.g.verified.cs
@@ -0,0 +1,11 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+ static partial void MapToB(this global::B target, global::A source)
+ {
+ target.Value = source.Value;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionUpdateMethodShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionExistingTargetShouldWork#Mapper.g.verified.cs
similarity index 100%
rename from test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionUpdateMethodShouldWork#Mapper.g.verified.cs
rename to test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionExistingTargetShouldWork#Mapper.g.verified.cs