diff --git a/README.md b/README.md
index 5da5cd8be4..7e57d72092 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,7 @@ The attributes defined in `Riok.Mapperly.Abstractions` can be used to customize
The `MapperAttribute` provides options to customize the generated mapper class.
The generated class name, the instance field name and the default enum mapping strategy is adjustable.
-### Copy behaviour
+#### Copy behaviour
By default, Mapperly does not create deep copies of objects to improve performance.
If an object can be directly assigned to the target, it will do so
@@ -56,9 +56,16 @@ To create deep copies, set the `UseDeepCloning` property on the `MapperAttribute
On each mapping method declaration property mappings can be customized.
If a property on the target has a different name than on the source, the `MapPropertyAttribute` can be applied.
-Flattening is not yet supported.
If a property should be ignored, the `MapperIgnoreAttribute` can be used.
+#### Flattening and unflattening
+
+It is pretty common to flatten objects during mapping, e.g. `Car.Make.Id => Car.MakeId`.
+Mapperly tries to figure out flattenings automatically by making use of the pascal case c# notation.
+If Mapperly can't resolve the target or source property correctly, it is possible to manually configure it by applying the `MapPropertyAttribute`
+by either using the source and target property path names as arrays or using a dot separated property access path string (e.g. `[MapProperty(Source = new[] { nameof(Car), nameof(Car.Make), nameof(Car.Make.Id) }, Target = new[] { nameof(Car), nameof(Car.MakeId) })]` or `[MapProperty(Source = "Car.Make.Id", Target = "Car.MakeId")]`).
+Note: unflattening is not yet automatically configured by Mapperly and needs to be configured manually via `MapPropertyAttribute`.
+
#### Enum
The enum mapping can be customized by setting the strategy to use.
diff --git a/Riok.Mapperly.sln b/Riok.Mapperly.sln
index ce824928fa..f52f25c37b 100644
--- a/Riok.Mapperly.sln
+++ b/Riok.Mapperly.sln
@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Abstractions"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Tests", "test\Riok.Mapperly.Tests\Riok.Mapperly.Tests.csproj", "{284E2122-CE48-4A5A-A045-3A3F941DA5C3}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Abstractions.Test", "test\Riok.Mapperly.Abstractions.Test\Riok.Mapperly.Abstractions.Test.csproj", "{C3C40A0A-168F-4A66-B9F9-FC80D2F26306}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -40,11 +42,16 @@ Global
{284E2122-CE48-4A5A-A045-3A3F941DA5C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{284E2122-CE48-4A5A-A045-3A3F941DA5C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{284E2122-CE48-4A5A-A045-3A3F941DA5C3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C3C40A0A-168F-4A66-B9F9-FC80D2F26306}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C3C40A0A-168F-4A66-B9F9-FC80D2F26306}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C3C40A0A-168F-4A66-B9F9-FC80D2F26306}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C3C40A0A-168F-4A66-B9F9-FC80D2F26306}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{CB991FD7-B9B7-47C0-A060-66EBE136DBDA} = {B65AF89A-4A3B-473C-83C8-5F0CB0EED30E}
{FDA97A46-DB21-4B72-9958-6D61C508B1CD} = {3598BE50-28D5-4BF4-BEA7-09E5FEA2910C}
{E45D5E6D-8CC9-4DAD-8E1C-723625475744} = {B65AF89A-4A3B-473C-83C8-5F0CB0EED30E}
{284E2122-CE48-4A5A-A045-3A3F941DA5C3} = {3598BE50-28D5-4BF4-BEA7-09E5FEA2910C}
+ {C3C40A0A-168F-4A66-B9F9-FC80D2F26306} = {3598BE50-28D5-4BF4-BEA7-09E5FEA2910C}
EndGlobalSection
EndGlobal
diff --git a/src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs b/src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs
index 61b82788c0..e0aa7bc970 100644
--- a/src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs
+++ b/src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs
@@ -6,12 +6,25 @@ namespace Riok.Mapperly.Abstractions;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class MapPropertyAttribute : Attribute
{
+ private const string PropertyAccessSeparatorStr = ".";
+ private const char PropertyAccessSeparator = '.';
+
///
/// Maps a specified source property to the specified target property.
///
- /// The name of the source property. The use of `nameof()` is encouraged.
- /// The name of the target property. The use of `nameof()` is encouraged.
+ /// The name of the source property. The use of `nameof()` is encouraged. A path can be specified by joining property names with a '.'.
+ /// The name of the target property. The use of `nameof()` is encouraged. A path can be specified by joining property names with a '.'.
public MapPropertyAttribute(string source, string target)
+ : this(source.Split(PropertyAccessSeparator), target.Split(PropertyAccessSeparator))
+ {
+ }
+
+ ///
+ /// Maps a specified source property to the specified target property.
+ ///
+ /// The path of the source property. The use of `nameof()` is encouraged.
+ /// The path of the target property. The use of `nameof()` is encouraged.
+ public MapPropertyAttribute(string[] source, string[] target)
{
Source = source;
Target = target;
@@ -20,10 +33,20 @@ public MapPropertyAttribute(string source, string target)
///
/// Gets the name of the source property.
///
- public string Source { get; }
+ public IReadOnlyCollection Source { get; }
+
+ ///
+ /// Gets the full name of the source property path.
+ ///
+ public string SourceFullName => string.Join(PropertyAccessSeparatorStr, Source);
///
/// Gets the name of the target property.
///
- public string Target { get; }
+ public IReadOnlyCollection Target { get; }
+
+ ///
+ /// Gets the full name of the target property path.
+ ///
+ public string TargetFullName => string.Join(PropertyAccessSeparatorStr, Target);
}
diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
index ffb804db8f..82203545a1 100644
--- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
+++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
@@ -12,3 +12,7 @@ RMG005 | Mapper | Error | Mapping target property not found.
RMG006 | Mapper | Error | Mapping source property not found.
RMG007 | Mapper | Error | Could not map property.
RMG008 | Mapper | Error | Could not create mapping.
+RMG009 | Mapper | Info | Can not map to read only property.
+RMG010 | Mapper | Info | Can not map from write only property.
+RMG011 | Mapper | Info | Can not map to write only property path.
+RMG012 | Mapper | Info | Mapping source property not found.
diff --git a/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs b/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
index 1696c8f7f8..c6eeafb606 100644
--- a/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
+++ b/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
@@ -31,7 +31,7 @@ public static IEnumerable Access(Compilation compilation, ISymbol symbol)
{
var attr = (T)Activator.CreateInstance(
attrType,
- BuildConstructorArguments(attrData));
+ BuildArgumentValues(attrData.ConstructorArguments).ToArray());
foreach (var namedArgument in attrData.NamedArguments)
{
@@ -46,24 +46,50 @@ public static IEnumerable Access(Compilation compilation, ISymbol symbol)
}
}
- private static object?[] BuildConstructorArguments(AttributeData attrData)
+ private static IEnumerable BuildArgumentValues(IEnumerable values)
{
- return attrData.ConstructorArguments
- .Select(arg =>
- {
- if (arg.Value == null)
- return null;
-
- // box enum values to resolve correct ctor
- if (arg.Type is not INamedTypeSymbol namedType || namedType.EnumUnderlyingType == null)
- return arg.Value;
-
- var assemblyName = arg.Type!.ContainingAssembly.Name;
- var qualifiedTypeName = Assembly.CreateQualifiedName(assemblyName, arg.Type.ToDisplayString());
- return Type.GetType(qualifiedTypeName) is { } type
- ? Enum.ToObject(type, arg.Value)
- : arg.Value;
- })
- .ToArray();
+ return values.Select(arg => arg.Kind switch
+ {
+ _ when arg.IsNull => null,
+ TypedConstantKind.Enum => GetEnumValue(arg),
+ TypedConstantKind.Array => BuildArrayValue(arg),
+ TypedConstantKind.Primitive => arg.Value,
+ _ => throw new ArgumentOutOfRangeException(
+ $"{nameof(AttributeDataAccessor)} does not support constructor arguments of kind {arg.Kind.ToString()}"),
+ });
+ }
+
+ private static object?[] BuildArrayValue(TypedConstant arg)
+ {
+ var arrayTypeSymbol = arg.Type as IArrayTypeSymbol
+ ?? throw new InvalidOperationException("Array typed constant is not of type " + nameof(IArrayTypeSymbol));
+
+ var elementType = GetReflectionType(arrayTypeSymbol.ElementType);
+
+ var values = BuildArgumentValues(arg.Values).ToArray();
+ var typedValues = Array.CreateInstance(elementType, values.Length);
+ Array.Copy(values, typedValues, values.Length);
+ return (object?[])typedValues;
}
+
+ private static object? GetEnumValue(TypedConstant arg)
+ {
+ var enumType = GetReflectionType(arg.Type ?? throw new InvalidOperationException("Type is null"));
+ return arg.Value == null
+ ? null
+ : Enum.ToObject(enumType, arg.Value);
+ }
+
+ private static Type GetReflectionType(ITypeSymbol type)
+ {
+ // other special types not yet supported since they are not used yet.
+ if (type.SpecialType == SpecialType.System_String)
+ return typeof(string);
+
+ var assemblyName = type.ContainingAssembly.Name;
+ var qualifiedTypeName = Assembly.CreateQualifiedName(assemblyName, type.ToDisplayString());
+ return Type.GetType(qualifiedTypeName)
+ ?? throw new InvalidOperationException($"Type {qualifiedTypeName} not found");
+ }
+
}
diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
index 9543f784c6..63a2c3263b 100644
--- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
@@ -2,7 +2,7 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.MappingBuilder;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
namespace Riok.Mapperly.Descriptors;
diff --git a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs
index 94a364182d..9dcb618255 100644
--- a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs
+++ b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
namespace Riok.Mapperly.Descriptors;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs
index 819f1e109d..98b7944341 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
namespace Riok.Mapperly.Descriptors.MappingBuilder;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs
index f2cb57f391..b82470b1d6 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/DirectAssignmentMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/DirectAssignmentMappingBuilder.cs
index 6f52f04266..fe0bfe106f 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/DirectAssignmentMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/DirectAssignmentMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
namespace Riok.Mapperly.Descriptors.MappingBuilder;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs
index 0a174fbbe8..43f492753a 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs
@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumToStringMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumToStringMappingBuilder.cs
index f46d834c35..125ca04ae8 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumToStringMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumToStringMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
namespace Riok.Mapperly.Descriptors.MappingBuilder;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumerableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumerableMappingBuilder.cs
index 715af04ad3..4ab2505e83 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumerableMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumerableMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ExplicitCastMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ExplicitCastMappingBuilder.cs
index dc366d6366..cfba194c83 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/ExplicitCastMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ExplicitCastMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis.CSharp;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
namespace Riok.Mapperly.Descriptors.MappingBuilder;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ImplicitCastMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ImplicitCastMappingBuilder.cs
index e8541a92de..c80547ac09 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/ImplicitCastMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ImplicitCastMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis.CSharp;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
namespace Riok.Mapperly.Descriptors.MappingBuilder;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/NullableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/NullableMappingBuilder.cs
index 22f62509d5..c0831a0dd3 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/NullableMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/NullableMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs
index da0a13d335..f7a5c94aa3 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
+using Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
@@ -21,12 +22,15 @@ public static class ObjectPropertyMappingBuilder
public static void BuildMappingBody(MappingBuilderContext ctx, ObjectPropertyMapping mapping)
{
+ var mappingCtx = new ObjectPropertyMappingBuilderContext(ctx, mapping);
+
var ignoredTargetProperties = ctx.ListConfiguration()
.Select(x => x.Target)
.ToHashSet();
- var nameMappings = ctx.ListConfiguration()
- .ToDictionary(x => x.Target, x => x.Source);
+ var propertyConfigsByRootTargetName = ctx.ListConfiguration()
+ .GroupBy(x => x.Target.First())
+ .ToDictionary(x => x.Key, x => x.ToList());
var targetProperties = mapping.TargetType
.GetAllMembers()
@@ -38,93 +42,246 @@ public static void BuildMappingBody(MappingBuilderContext ctx, ObjectPropertyMap
if (ignoredTargetProperties.Remove(targetProperty.Name))
continue;
- var mappingNameWasManuallyConfigured = nameMappings.Remove(targetProperty.Name, out var sourcePropertyName);
- sourcePropertyName ??= targetProperty.Name;
-
- var sourceProperty = FindSourceProperty(mapping.SourceType, sourcePropertyName);
- if (sourceProperty != null)
+ if (propertyConfigsByRootTargetName.Remove(targetProperty.Name, out var propertyConfigs))
{
- if (BuildPropertyMapping(ctx, mapping, sourceProperty, targetProperty) is { } propertyMapping)
+ // add all configured mappings
+ // order by target path count to map less nested items first (otherwise they would overwrite all others)
+ // eg. target.A = source.B should be mapped before target.A.Id = source.B.Id
+ foreach (var config in propertyConfigs.OrderBy(x => x.Target.Count))
{
- mapping.AddPropertyMapping(propertyMapping);
+ BuildPropertyMapping(mappingCtx, config.Source, config.Target, true);
}
+
continue;
}
- if (mappingNameWasManuallyConfigured)
+ // only try other namings if the property was not found,
+ // ignore all other results
+ var targetPropertyPath = new[] { targetProperty.Name };
+ var targetPropFound = false;
+ foreach (var sourcePropertyCandidate in MemberPathCandidateBuilder.BuildMemberPathCandidates(targetProperty.Name))
+ {
+ if (BuildPropertyMapping(mappingCtx, sourcePropertyCandidate.ToList(), targetPropertyPath) is not ValidationResult.PropertyNotFound)
+ {
+ targetPropFound = true;
+ break;
+ }
+ }
+
+ // target property couldn't be found
+ // add a diagnostic.
+ if (!targetPropFound)
{
ctx.ReportDiagnostic(
- DiagnosticDescriptors.ConfiguredMappingSourcePropertyNotFound,
- sourcePropertyName,
- mapping.TargetType);
+ DiagnosticDescriptors.MappingSourcePropertyNotFound,
+ targetProperty.Name,
+ mapping.SourceType);
}
}
- AddUnmatchedIgnoredPropertiesDiagnostics(ctx, ignoredTargetProperties, mapping);
- AddUnmatchedTargetPropertiesDiagnostics(ctx, nameMappings.Keys, mapping);
+ AddUnmatchedIgnoredPropertiesDiagnostics(mappingCtx, ignoredTargetProperties);
+ AddUnmatchedTargetPropertiesDiagnostics(mappingCtx, propertyConfigsByRootTargetName.Values.SelectMany(x => x));
}
- private static void AddUnmatchedTargetPropertiesDiagnostics(
- MappingBuilderContext ctx,
- IEnumerable propertyNames,
- ObjectPropertyMapping mapping)
+ private static ValidationResult BuildPropertyMapping(
+ ObjectPropertyMappingBuilderContext ctx,
+ IReadOnlyCollection sourcePath,
+ IReadOnlyCollection targetPath,
+ bool configuredTargetPropertyPath = false)
{
- foreach (var propertyName in propertyNames)
+ var targetPropertyPath = new PropertyPath(FindPropertyPath(ctx.Mapping.TargetType, targetPath).ToList());
+ var sourcePropertyPath = new PropertyPath(FindPropertyPath(ctx.Mapping.SourceType, sourcePath).ToList());
+
+ var validationResult = ValidateMapping(
+ ctx,
+ sourcePropertyPath,
+ targetPropertyPath,
+ sourcePath,
+ targetPath,
+ configuredTargetPropertyPath);
+ if (validationResult != ValidationResult.Ok)
+ return validationResult;
+
+ // nullability is handled inside the property mapping
+ var delegateMapping = ctx.BuilderContext.FindMapping(sourcePropertyPath.Member.Type.UpgradeNullable(), targetPropertyPath.Member.Type.UpgradeNullable())
+ ?? ctx.BuilderContext.FindOrBuildMapping(sourcePropertyPath.Member.Type.NonNullable(), targetPropertyPath.Member.Type.NonNullable());
+
+ // couldn't build the mapping
+ if (delegateMapping == null)
{
- ctx.ReportDiagnostic(
- DiagnosticDescriptors.ConfiguredMappingTargetPropertyNotFound,
- propertyName,
- mapping.TargetType);
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.CouldNotMapProperty,
+ ctx.Mapping.SourceType,
+ sourcePropertyPath.FullName,
+ sourcePropertyPath.Member.Type,
+ ctx.Mapping.TargetType,
+ targetPropertyPath.FullName,
+ targetPropertyPath.Member.Type);
+ return ValidationResult.CannotMapTypes;
+ }
+
+ // no member of the source path is nullable, no null handling needed
+ if (!sourcePropertyPath.IsAnyNullable())
+ {
+ ctx.AddPropertyMapping(new PropertyMapping(
+ sourcePropertyPath,
+ targetPropertyPath,
+ delegateMapping,
+ false));
+ return ValidationResult.Ok;
}
+
+ // the source is nullable, or the mapping is a direct assignment and the target allows nulls
+ // access the source in a null save matter (via ?.) but no other special handling required.
+ if (delegateMapping.SourceType.IsNullable() || delegateMapping is DirectAssignmentMapping && targetPropertyPath.Member.IsNullable())
+ {
+ ctx.AddPropertyMapping(new PropertyMapping(
+ sourcePropertyPath,
+ targetPropertyPath,
+ delegateMapping,
+ true));
+ return ValidationResult.Ok;
+ }
+
+ // additional null condition check
+ // (only map if source is not null, else may throw depending on settings)
+ ctx.AddNullDelegatePropertyMapping(new PropertyMapping(
+ sourcePropertyPath,
+ targetPropertyPath,
+ delegateMapping,
+ false));
+ return ValidationResult.Ok;
}
- private static void AddUnmatchedIgnoredPropertiesDiagnostics(
- MappingBuilderContext ctx,
- HashSet ignoredTargetProperties,
- ObjectPropertyMapping mapping)
+ private static ValidationResult ValidateMapping(
+ ObjectPropertyMappingBuilderContext ctx,
+ PropertyPath sourcePropertyPath,
+ PropertyPath targetPropertyPath,
+ IReadOnlyCollection configuredSourcePropertyPath,
+ IReadOnlyCollection configuredTargetPropertyPath,
+ bool reportDiagnosticIfPropertyNotFound)
{
- foreach (var notFoundIgnoredProperty in ignoredTargetProperties)
+ // the path parts don't match, not all target properties could be found
+ if (configuredTargetPropertyPath.Count != targetPropertyPath.Path.Count)
{
- ctx.ReportDiagnostic(
- DiagnosticDescriptors.IgnoredPropertyNotFound,
- notFoundIgnoredProperty,
- mapping.TargetType);
+ if (reportDiagnosticIfPropertyNotFound)
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.ConfiguredMappingTargetPropertyNotFound,
+ string.Join(PropertyPath.PropertyAccessSeparator, configuredTargetPropertyPath),
+ ctx.Mapping.TargetType);
+ }
+ return ValidationResult.PropertyNotFound;
+ }
+
+ // the path parts don't match, not all source properties could be found
+ if (configuredSourcePropertyPath.Count != sourcePropertyPath.Path.Count)
+ {
+ if (reportDiagnosticIfPropertyNotFound)
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.ConfiguredMappingSourcePropertyNotFound,
+ string.Join(PropertyPath.PropertyAccessSeparator, configuredSourcePropertyPath),
+ ctx.Mapping.SourceType);
+ }
+ return ValidationResult.PropertyNotFound;
+ }
+
+ // the target property path is readonly
+ if (targetPropertyPath.Member.IsReadOnly)
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.CanNotMapToReadOnlyProperty,
+ ctx.Mapping.SourceType,
+ sourcePropertyPath.FullName,
+ sourcePropertyPath.Member.Type,
+ ctx.Mapping.TargetType,
+ targetPropertyPath.FullName,
+ targetPropertyPath.Member.Type);
+ return ValidationResult.PropertyHasUnexpectedSpecification;
+ }
+
+ // a target property path part is write only
+ if (targetPropertyPath.ObjectPath.Any(p => p.IsWriteOnly))
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.CanNotMapToWriteOnlyPropertyPath,
+ ctx.Mapping.SourceType,
+ sourcePropertyPath.FullName,
+ sourcePropertyPath.Member.Type,
+ ctx.Mapping.TargetType,
+ targetPropertyPath.FullName,
+ targetPropertyPath.Member.Type);
+ return ValidationResult.PropertyHasUnexpectedSpecification;
+ }
+
+ // a source property path is write only
+ if (sourcePropertyPath.Path.Any(p => p.IsWriteOnly))
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.CanNotMapFromWriteOnlyProperty,
+ ctx.Mapping.SourceType,
+ sourcePropertyPath.FullName,
+ sourcePropertyPath.Member.Type,
+ ctx.Mapping.TargetType,
+ targetPropertyPath.FullName,
+ targetPropertyPath.Member.Type);
+ return ValidationResult.PropertyHasUnexpectedSpecification;
+ }
+
+ return ValidationResult.Ok;
+ }
+
+ private static IEnumerable FindPropertyPath(ITypeSymbol type, IEnumerable path)
+ {
+ foreach (var name in path)
+ {
+ if (FindProperty(type, name) is not { } property)
+ break;
+
+ type = property.Type;
+ yield return property;
}
}
- private static IPropertySymbol? FindSourceProperty(ITypeSymbol source, string name)
+ private static IPropertySymbol? FindProperty(ITypeSymbol type, string name)
{
- return source.GetAllMembers(name)
+ return type.GetAllMembers(name)
.OfType()
.FirstOrDefault(p => !p.IsStatic);
}
- private static PropertyMapping? BuildPropertyMapping(
- MappingBuilderContext ctx,
- ObjectPropertyMapping mapping,
- IPropertySymbol sourceProperty,
- IPropertySymbol targetProperty)
+ private static void AddUnmatchedTargetPropertiesDiagnostics(
+ ObjectPropertyMappingBuilderContext ctx,
+ IEnumerable unmatchedConfiguredProperties)
{
- if (targetProperty.IsReadOnly)
- return null;
+ foreach (var propertyConfig in unmatchedConfiguredProperties)
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.ConfiguredMappingTargetPropertyNotFound,
+ propertyConfig.TargetFullName,
+ ctx.Mapping.TargetType);
+ }
+ }
- if (sourceProperty.IsWriteOnly)
- return null;
+ private static void AddUnmatchedIgnoredPropertiesDiagnostics(
+ ObjectPropertyMappingBuilderContext ctx,
+ HashSet ignoredTargetProperties)
+ {
+ foreach (var notFoundIgnoredProperty in ignoredTargetProperties)
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.IgnoredPropertyNotFound,
+ notFoundIgnoredProperty,
+ ctx.Mapping.TargetType);
+ }
+ }
- // nullability is handled inside the property mapping
- var delegateMapping = ctx.FindMapping(sourceProperty.Type.UpgradeNullable(), targetProperty.Type.UpgradeNullable())
- ?? ctx.FindOrBuildMapping(sourceProperty.Type.NonNullable(), targetProperty.Type.NonNullable());
- if (delegateMapping != null)
- return new PropertyMapping(sourceProperty, targetProperty, delegateMapping, ctx.MapperConfiguration.ThrowOnPropertyMappingNullMismatch);
-
- ctx.ReportDiagnostic(
- DiagnosticDescriptors.CouldNotMapProperty,
- mapping.SourceType,
- sourceProperty.Name,
- sourceProperty.Type,
- mapping.TargetType,
- targetProperty.Name,
- targetProperty.Type);
- return null;
+ private enum ValidationResult
+ {
+ Ok,
+ PropertyNotFound,
+ PropertyHasUnexpectedSpecification,
+ CannotMapTypes,
}
}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilderContext.cs
new file mode 100644
index 0000000000..c4187deaf7
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilderContext.cs
@@ -0,0 +1,81 @@
+using Riok.Mapperly.Descriptors.Mappings;
+using Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
+using Riok.Mapperly.Diagnostics;
+using Riok.Mapperly.Helpers;
+
+namespace Riok.Mapperly.Descriptors.MappingBuilder;
+
+public class ObjectPropertyMappingBuilderContext
+{
+ private readonly Dictionary _nullDelegateMappings = new();
+
+ public ObjectPropertyMappingBuilderContext(MappingBuilderContext builderContext, ObjectPropertyMapping mapping)
+ {
+ BuilderContext = builderContext;
+ Mapping = mapping;
+ }
+
+ public MappingBuilderContext BuilderContext { get; }
+
+ public ObjectPropertyMapping Mapping { get; }
+
+ public void AddPropertyMapping(PropertyMapping propertyMapping)
+ => AddPropertyMapping(Mapping, propertyMapping);
+
+ public void AddNullDelegatePropertyMapping(PropertyMapping propertyMapping)
+ {
+ var nullConditionSourcePath = new PropertyPath(propertyMapping.SourcePath.PathWithoutTrailingNonNullable().ToList());
+ var container = GetOrCreateNullDelegateMappingForPath(nullConditionSourcePath);
+ AddPropertyMapping(container, propertyMapping);
+ }
+
+ private void AddPropertyMapping(IPropertyMappingContainer container, PropertyMapping mapping)
+ {
+ container.AddPropertyMappings(BuildNullPropertyInitializers(mapping.TargetPath));
+ container.AddPropertyMapping(mapping);
+ }
+
+ private IEnumerable BuildNullPropertyInitializers(PropertyPath path)
+ {
+ foreach (var nullableTrailPath in path.ObjectPathNullableSubPaths())
+ {
+ var nullablePath = new PropertyPath(nullableTrailPath);
+ var type = nullablePath.Member.Type;
+ if (!type.HasAccessibleParameterlessConstructor())
+ {
+ BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.NoParameterlessConstructorFound,
+ type);
+ continue;
+ }
+
+ yield return new PropertyNullInitializerDelegateMapping(nullablePath);
+ }
+ }
+
+ private PropertyNullDelegateMapping GetOrCreateNullDelegateMappingForPath(PropertyPath nullConditionSourcePath)
+ {
+ // if there is already an exact match return that
+ if (_nullDelegateMappings.TryGetValue(nullConditionSourcePath, out var mapping))
+ return mapping;
+
+ IPropertyMappingContainer parentMapping = Mapping;
+
+ // try to reuse parent path mappings and wrap inside them
+ foreach (var nullablePath in nullConditionSourcePath.ObjectPathNullableSubPaths().Reverse())
+ {
+ if (_nullDelegateMappings.TryGetValue(new PropertyPath(nullablePath), out var parentMappingHolder))
+ {
+ parentMapping = parentMappingHolder;
+ }
+ }
+
+ mapping = new PropertyNullDelegateMapping(
+ nullConditionSourcePath,
+ parentMapping,
+ BuilderContext.MapperConfiguration.ThrowOnPropertyMappingNullMismatch);
+ _nullDelegateMappings[nullConditionSourcePath] = mapping;
+ parentMapping.AddPropertyMapping(mapping);
+ return mapping;
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ParseMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ParseMappingBuilder.cs
index bd8add9def..a64cfe38f5 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/ParseMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ParseMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
namespace Riok.Mapperly.Descriptors.MappingBuilder;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/SpecialTypeMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/SpecialTypeMappingBuilder.cs
index 11132cc053..89faa38c9c 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/SpecialTypeMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/SpecialTypeMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
namespace Riok.Mapperly.Descriptors.MappingBuilder;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/StringToEnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/StringToEnumMappingBuilder.cs
index f22cd68c83..9c0769fb43 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/StringToEnumMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/StringToEnumMappingBuilder.cs
@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
namespace Riok.Mapperly.Descriptors.MappingBuilder;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ToStringMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ToStringMappingBuilder.cs
index 7cc1005345..ef6f421dcd 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/ToStringMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ToStringMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
namespace Riok.Mapperly.Descriptors.MappingBuilder;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs
index 80ef43a143..4e40a222d0 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
index 24c06b6803..d4c1fbf82e 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Configuration;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
namespace Riok.Mapperly.Descriptors;
@@ -44,19 +44,6 @@ public class MappingBuilderContext : SimpleMappingBuilderContext
public TypeMapping? FindOrBuildMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
=> _builder.FindOrBuildMapping(sourceType, targetType);
- ///
- /// Tries to find an existing mapping for the provided types.
- /// If none is found, a new one is created.
- /// If a new mapping is created, it is not added to the mapping descriptor (should only be used as a delegate to another mapping)
- /// and is therefore not accessible by other mappings.
- /// Configuration / the user symbol is passed from the caller.
- ///
- /// The source type.
- /// The target type.
- /// The created mapping or null if none could be created.
- public TypeMapping? FindOrBuildDelegateMapping(ITypeSymbol source, ITypeSymbol target)
- => _builder.FindOrBuildDelegateMapping(_userSymbol, source, target);
-
///
/// Tries to build a new mapping for the given types.
/// The built mapping is not added to the mapping descriptor (should only be used as a delegate to another mapping)
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/ArrayCloneMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ArrayCloneMapping.cs
similarity index 94%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/ArrayCloneMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/ArrayCloneMapping.cs
index 18228cb9db..2da9d023e7 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/ArrayCloneMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/ArrayCloneMapping.cs
@@ -3,7 +3,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping from an array to an array of the same type by using Array.Clone.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/CastMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/CastMapping.cs
similarity index 93%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/CastMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/CastMapping.cs
index f8fa24391c..35e4aa97d3 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/CastMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/CastMapping.cs
@@ -2,7 +2,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a cast mapping.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/CtorMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/CtorMapping.cs
similarity index 93%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/CtorMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/CtorMapping.cs
index 7213f35708..11b547fe71 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/CtorMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/CtorMapping.cs
@@ -4,7 +4,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping where the target type has the source as single ctor argument.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/DirectAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/DirectAssignmentMapping.cs
similarity index 89%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/DirectAssignmentMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/DirectAssignmentMapping.cs
index eb19e1d084..d91ea5fa42 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/DirectAssignmentMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/DirectAssignmentMapping.cs
@@ -1,7 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a direct assignment mapping.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/EnumFromStringMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/EnumFromStringMapping.cs
similarity index 98%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/EnumFromStringMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/EnumFromStringMapping.cs
index 581ebd30f2..a63d0df949 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/EnumFromStringMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/EnumFromStringMapping.cs
@@ -4,7 +4,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping from a string to an enum.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/EnumNameMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/EnumNameMapping.cs
similarity index 97%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/EnumNameMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/EnumNameMapping.cs
index db5c376ae8..921db83500 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/EnumNameMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/EnumNameMapping.cs
@@ -3,7 +3,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping from an enum to another enum by using their names.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/EnumToStringMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/EnumToStringMapping.cs
similarity index 97%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/EnumToStringMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/EnumToStringMapping.cs
index 41d183e38c..b1a463887b 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/EnumToStringMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/EnumToStringMapping.cs
@@ -4,7 +4,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping from an enum to a string.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/ForEachAddDictionaryMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddDictionaryMapping.cs
similarity index 97%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/ForEachAddDictionaryMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/ForEachAddDictionaryMapping.cs
index 34d4346f58..1f46edca44 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/ForEachAddDictionaryMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddDictionaryMapping.cs
@@ -3,7 +3,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a foreach dictionary mapping which works by looping through the source,
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/ForEachAddEnumerableMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableMapping.cs
similarity index 96%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/ForEachAddEnumerableMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableMapping.cs
index 60c131a79d..2195cb1674 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/ForEachAddEnumerableMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableMapping.cs
@@ -3,7 +3,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a foreach enumerable mapping which works by looping through the source,
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/IUserMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/IUserMapping.cs
similarity index 77%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/IUserMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/IUserMapping.cs
index 4fc43bafd7..53b696806b 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/IUserMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/IUserMapping.cs
@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// A user defined / implemented mapping.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/LinqEnumerableMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/LinqEnumerableMapping.cs
similarity index 97%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/LinqEnumerableMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/LinqEnumerableMapping.cs
index 64a6cef1e7..a401317521 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/LinqEnumerableMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/LinqEnumerableMapping.cs
@@ -3,7 +3,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents an enumerable mapping which works by using linq (select + collect).
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
similarity index 98%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/MethodMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
index 4d930f1daa..b549f8df89 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/MethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
@@ -4,7 +4,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping which is not a single expression but an entire method.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/NewInstanceObjectPropertyMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectPropertyMapping.cs
similarity index 94%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/NewInstanceObjectPropertyMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectPropertyMapping.cs
index 748d292bc6..043b11a911 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/NewInstanceObjectPropertyMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectPropertyMapping.cs
@@ -3,7 +3,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
public class NewInstanceObjectPropertyMapping : ObjectPropertyMapping
{
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs
similarity index 97%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs
index 1eed32bda3..acc9c97cdb 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs
@@ -4,7 +4,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Null aware delegate mapping. Abstracts handling null values of the delegated mapping.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs
similarity index 97%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMethodMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs
index c81390da28..dac5c589a9 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs
@@ -3,7 +3,7 @@
using Riok.Mapperly.Helpers;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Null aware delegate mapping for s.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/NullFallbackValue.cs b/src/Riok.Mapperly/Descriptors/Mappings/NullFallbackValue.cs
similarity index 70%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/NullFallbackValue.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/NullFallbackValue.cs
index 4e3e6e24b1..3ed7b5259f 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/NullFallbackValue.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/NullFallbackValue.cs
@@ -1,4 +1,4 @@
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
public enum NullFallbackValue
{
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ObjectPropertyMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ObjectPropertyMapping.cs
new file mode 100644
index 0000000000..db798d5269
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/ObjectPropertyMapping.cs
@@ -0,0 +1,35 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
+
+namespace Riok.Mapperly.Descriptors.Mappings;
+
+///
+/// Represents a complex object mapping implemented in its own method.
+/// Maps each property from the source to the target.
+///
+public abstract class ObjectPropertyMapping : MethodMapping, IPropertyMappingContainer
+{
+ private readonly HashSet _mappings = new();
+
+ protected ObjectPropertyMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : base(sourceType, targetType)
+ {
+ }
+
+ public void AddPropertyMapping(IPropertyMapping mapping)
+ => _mappings.Add(mapping);
+
+ public void AddPropertyMappings(IEnumerable mappings)
+ {
+ foreach (var mapping in mappings)
+ {
+ _mappings.Add(mapping);
+ }
+ }
+
+ public bool HasPropertyMapping(IPropertyMapping mapping)
+ => _mappings.Contains(mapping);
+
+ internal IEnumerable BuildBody(ExpressionSyntax source, ExpressionSyntax target)
+ => _mappings.Select(x => x.Build(source, target));
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyMapping.cs
new file mode 100644
index 0000000000..1d2a1c6e71
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyMapping.cs
@@ -0,0 +1,13 @@
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
+
+///
+/// Represents a property mapping or a container of property mappings.
+///
+public interface IPropertyMapping
+{
+ StatementSyntax Build(
+ ExpressionSyntax sourceAccess,
+ ExpressionSyntax targetAccess);
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyMappingContainer.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyMappingContainer.cs
new file mode 100644
index 0000000000..6acfeec5bc
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyMappingContainer.cs
@@ -0,0 +1,13 @@
+namespace Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
+
+///
+/// Represents a container of several property mappings.
+///
+public interface IPropertyMappingContainer
+{
+ bool HasPropertyMapping(IPropertyMapping mapping);
+
+ void AddPropertyMapping(IPropertyMapping mapping);
+
+ void AddPropertyMappings(IEnumerable mappings);
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyMapping.cs
new file mode 100644
index 0000000000..0ca429a997
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyMapping.cs
@@ -0,0 +1,88 @@
+using System.Diagnostics;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
+
+///
+/// Represents a simple property mapping (eg. target.A = source.B)
+///
+[DebuggerDisplay("PropertyMapping({SourcePath.FullName} => {TargetPath.FullName})")]
+public class PropertyMapping : IPropertyMapping
+{
+ private readonly TypeMapping _mapping;
+ private readonly bool _nullConditionalSourceAccess;
+
+ public PropertyMapping(
+ PropertyPath sourcePath,
+ PropertyPath targetPath,
+ TypeMapping mapping,
+ bool nullConditionalSourceAccess)
+ {
+ SourcePath = sourcePath;
+ TargetPath = targetPath;
+ _mapping = mapping;
+ _nullConditionalSourceAccess = nullConditionalSourceAccess;
+ }
+
+ public PropertyPath SourcePath { get; }
+
+ public PropertyPath TargetPath { get; }
+
+ public StatementSyntax Build(
+ ExpressionSyntax sourceAccess,
+ ExpressionSyntax targetAccess)
+ {
+ var sourcePropertyAccess = SourcePath.BuildAccess(sourceAccess, true, _nullConditionalSourceAccess);
+ var targetPropertyAccess = TargetPath.BuildAccess(targetAccess);
+ var mappedValue = _mapping.Build(sourcePropertyAccess);
+
+ // target.Property = mappedValue;
+ var assignment = AssignmentExpression(
+ SyntaxKind.SimpleAssignmentExpression,
+ targetPropertyAccess,
+ mappedValue);
+ return ExpressionStatement(assignment);
+ }
+
+ 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((PropertyMapping)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = _mapping.GetHashCode();
+ hashCode = (hashCode * 397) ^ SourcePath.GetHashCode();
+ hashCode = (hashCode * 397) ^ TargetPath.GetHashCode();
+ hashCode = (hashCode * 397) ^ _nullConditionalSourceAccess.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ public static bool operator ==(PropertyMapping? left, PropertyMapping? right)
+ => Equals(left, right);
+
+ public static bool operator !=(PropertyMapping? left, PropertyMapping? right)
+ => !Equals(left, right);
+
+ protected bool Equals(PropertyMapping other)
+ {
+ return _mapping.Equals(other._mapping)
+ && SourcePath.Equals(other.SourcePath)
+ && TargetPath.Equals(other.TargetPath)
+ && _nullConditionalSourceAccess == other._nullConditionalSourceAccess;
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullDelegateMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullDelegateMapping.cs
new file mode 100644
index 0000000000..66375011ed
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullDelegateMapping.cs
@@ -0,0 +1,99 @@
+using System.Diagnostics;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
+
+///
+/// a property mapping container, which performs a null check before the mappings.
+///
+[DebuggerDisplay("PropertyNullDelegateMapping({_nullConditionalSourcePath} != null)")]
+public class PropertyNullDelegateMapping : IPropertyMapping, IPropertyMappingContainer
+{
+ private readonly PropertyPath _nullConditionalSourcePath;
+ private readonly bool _throwInsteadOfConditionalNullMapping;
+ private readonly HashSet _delegateMappings = new();
+ private readonly IPropertyMappingContainer _parent;
+
+ public PropertyNullDelegateMapping(
+ PropertyPath nullConditionalSourcePath,
+ IPropertyMappingContainer parent,
+ bool throwInsteadOfConditionalNullMapping)
+ {
+ _nullConditionalSourcePath = nullConditionalSourcePath;
+ _throwInsteadOfConditionalNullMapping = throwInsteadOfConditionalNullMapping;
+ _parent = parent;
+ }
+
+ public StatementSyntax Build(
+ ExpressionSyntax sourceAccess,
+ ExpressionSyntax targetAccess)
+ {
+ // if (source.Value != null)
+ // target.Value = Map(Source.Name);
+ // else
+ // throw ...
+ var sourceNullConditionalAccess = _nullConditionalSourcePath.BuildAccess(sourceAccess, true, true, true);
+ var condition = IsNotNull(sourceNullConditionalAccess);
+ var elseClause = _throwInsteadOfConditionalNullMapping
+ ? ElseClause(Block(ExpressionStatement(ThrowNewArgumentNullException(sourceNullConditionalAccess))))
+ : null;
+
+ var mappings = _delegateMappings.Select(m => m.Build(sourceAccess, targetAccess)).ToList();
+ return IfStatement(condition, Block(mappings), elseClause);
+ }
+
+ public void AddPropertyMappings(IEnumerable mappings)
+ {
+ foreach (var mapping in mappings)
+ {
+ AddPropertyMapping(mapping);
+ }
+ }
+
+ public void AddPropertyMapping(IPropertyMapping mapping)
+ {
+ if (!HasPropertyMapping(mapping))
+ {
+ _delegateMappings.Add(mapping);
+ }
+ }
+
+ public bool HasPropertyMapping(IPropertyMapping mapping)
+ => _delegateMappings.Contains(mapping) || _parent.HasPropertyMapping(mapping);
+
+ 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((PropertyNullDelegateMapping)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (_nullConditionalSourcePath.GetHashCode() * 397) ^ _throwInsteadOfConditionalNullMapping.GetHashCode();
+ }
+ }
+
+ public static bool operator ==(PropertyNullDelegateMapping? left, PropertyNullDelegateMapping? right)
+ => Equals(left, right);
+
+ public static bool operator !=(PropertyNullDelegateMapping? left, PropertyNullDelegateMapping? right)
+ => !Equals(left, right);
+
+ protected bool Equals(PropertyNullDelegateMapping other)
+ {
+ return _nullConditionalSourcePath.Equals(other._nullConditionalSourcePath)
+ && _throwInsteadOfConditionalNullMapping == other._throwInsteadOfConditionalNullMapping;
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullInitializerDelegateMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullInitializerDelegateMapping.cs
new file mode 100644
index 0000000000..fe645a96c0
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullInitializerDelegateMapping.cs
@@ -0,0 +1,56 @@
+using System.Diagnostics;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
+
+///
+/// A property initializer which initializes null properties to a new objects.
+///
+[DebuggerDisplay("PropertyNullInitializerDelegateMapping({_pathToInitialize} ??= new())")]
+public class PropertyNullInitializerDelegateMapping : IPropertyMapping
+{
+ private readonly PropertyPath _pathToInitialize;
+
+ public PropertyNullInitializerDelegateMapping(PropertyPath pathToInitialize)
+ {
+ _pathToInitialize = pathToInitialize;
+ }
+
+ public StatementSyntax Build(ExpressionSyntax sourceAccess, ExpressionSyntax targetAccess)
+ {
+ // source.Value ??= new();
+ return ExpressionStatement(
+ AssignmentExpression(
+ SyntaxKind.CoalesceAssignmentExpression,
+ _pathToInitialize.BuildAccess(targetAccess),
+ ImplicitObjectCreationExpression()));
+ }
+
+ 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((PropertyNullInitializerDelegateMapping)obj);
+ }
+
+ public override int GetHashCode()
+ => _pathToInitialize.GetHashCode();
+
+ public static bool operator ==(PropertyNullInitializerDelegateMapping? left, PropertyNullInitializerDelegateMapping? right)
+ => Equals(left, right);
+
+ public static bool operator !=(PropertyNullInitializerDelegateMapping? left, PropertyNullInitializerDelegateMapping? right)
+ => !Equals(left, right);
+
+ protected bool Equals(PropertyNullInitializerDelegateMapping other)
+ => _pathToInitialize.Equals(other._pathToInitialize);
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyPath.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyPath.cs
new file mode 100644
index 0000000000..fc3de05191
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyPath.cs
@@ -0,0 +1,137 @@
+using System.Diagnostics;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Helpers;
+using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
+
+///
+/// Represents a set of properties to access a certain property.
+/// Eg. A.B.C
+///
+[DebuggerDisplay("{FullName}")]
+public class PropertyPath
+{
+ internal const string PropertyAccessSeparator = ".";
+ private const string NullableValueProperty = "Value";
+
+ private IPropertySymbol? _member;
+
+ public PropertyPath(IReadOnlyCollection path)
+ {
+ Path = path;
+ FullName = string.Join(PropertyAccessSeparator, Path.Select(x => x.Name));
+ }
+
+ public IReadOnlyCollection Path { get; }
+
+ ///
+ /// Gets the path without the very last element (the path of the object containing the ).
+ ///
+ public IEnumerable ObjectPath => Path.SkipLast();
+
+ ///
+ /// Gets the last part of the path or throws if there is none.
+ ///
+ public IPropertySymbol Member
+ {
+ get => _member ??= Path.Last();
+ }
+
+ ///
+ /// Gets the full name of the path (eg. A.B.C).
+ ///
+ public string FullName { get; }
+
+ ///
+ /// Builds a property path skipping trailing path items which are non nullable.
+ ///
+ /// The built path.
+ public IEnumerable PathWithoutTrailingNonNullable()
+ => Path.Reverse().SkipWhile(x => !x.IsNullable()).Reverse();
+
+ ///
+ /// Returns an element for each nullable sub-path of the .
+ /// If the is nullable, the entire is not returned.
+ ///
+ /// All nullable sub-paths of the .
+ public IEnumerable> ObjectPathNullableSubPaths()
+ {
+ var pathParts = new List(Path.Count);
+ foreach (var pathPart in ObjectPath)
+ {
+ pathParts.Add(pathPart);
+ if (!pathPart.IsNullable())
+ continue;
+
+ yield return pathParts;
+ }
+ }
+
+ public bool IsAnyNullable()
+ => Path.Any(p => p.IsNullable());
+
+ public ExpressionSyntax BuildAccess(
+ ExpressionSyntax baseAccess,
+ bool addValuePropertyOnNullable = false,
+ bool nullConditional = false,
+ bool skipTrailingNonNullable = false)
+ {
+ if (!nullConditional)
+ {
+ if (addValuePropertyOnNullable)
+ {
+ return Path.Aggregate(baseAccess, (a, b) => b.Type.IsNullableValueType()
+ ? MemberAccess(MemberAccess(a, b.Name), NullableValueProperty)
+ : MemberAccess(a, b.Name));
+ }
+
+ return Path.Aggregate(baseAccess, (a, b) => MemberAccess(a, b.Name));
+ }
+
+ var path = skipTrailingNonNullable
+ ? PathWithoutTrailingNonNullable()
+ : Path;
+
+ return path.AggregateWithPrevious(
+ baseAccess,
+ (expr, prevProp, prop) => prevProp?.IsNullable() == true
+ ? ConditionalAccess(expr, prop.Name)
+ : MemberAccess(expr, prop.Name));
+ }
+
+ 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((PropertyPath)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ var hc = 0;
+ foreach (var item in Path)
+ {
+ hc ^= SymbolEqualityComparer.Default.GetHashCode(item);
+ }
+
+ return hc;
+ }
+
+ public static bool operator ==(PropertyPath? left, PropertyPath? right)
+ => Equals(left, right);
+
+ public static bool operator !=(PropertyPath? left, PropertyPath? right)
+ => !Equals(left, right);
+
+ private bool Equals(PropertyPath other)
+ => Path.SequenceEqual(other.Path, SymbolEqualityComparer.IncludeNullability);
+}
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/SourceObjectMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/SourceObjectMethodMapping.cs
similarity index 93%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/SourceObjectMethodMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/SourceObjectMethodMapping.cs
index 82cff89cbc..0af7209a87 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/SourceObjectMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/SourceObjectMethodMapping.cs
@@ -3,7 +3,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping which works by invoking an instance method on the source object.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/StaticMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/StaticMethodMapping.cs
similarity index 92%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/StaticMethodMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/StaticMethodMapping.cs
index 54675ce630..4173922e68 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/StaticMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/StaticMethodMapping.cs
@@ -2,7 +2,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping which works by invoking a static method with the source as only argument.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/TypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/TypeMapping.cs
similarity index 94%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/TypeMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/TypeMapping.cs
index 640608bc57..52fb09e8b2 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/TypeMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/TypeMapping.cs
@@ -2,7 +2,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping to map from one type to another.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedExistingInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedExistingInstanceMethodMapping.cs
similarity index 97%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedExistingInstanceMethodMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/UserDefinedExistingInstanceMethodMapping.cs
index d5a3fcf0d3..803ed05a2f 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedExistingInstanceMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedExistingInstanceMethodMapping.cs
@@ -4,7 +4,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping method declared but not implemented by the user which reuses an existing target object instance.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedNewInstanceMethodMapping.cs
similarity index 96%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedNewInstanceMethodMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/UserDefinedNewInstanceMethodMapping.cs
index a35c5c7a93..13720e180a 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedNewInstanceMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedNewInstanceMethodMapping.cs
@@ -4,7 +4,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping method declared but not implemented by the user which results in a new target object instance.
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/UserImplementedMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserImplementedMethodMapping.cs
similarity index 96%
rename from src/Riok.Mapperly/Descriptors/TypeMappings/UserImplementedMethodMapping.cs
rename to src/Riok.Mapperly/Descriptors/Mappings/UserImplementedMethodMapping.cs
index ed3b974487..237ba4cc58 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/UserImplementedMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserImplementedMethodMapping.cs
@@ -4,7 +4,7 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-namespace Riok.Mapperly.Descriptors.TypeMappings;
+namespace Riok.Mapperly.Descriptors.Mappings;
///
/// Represents a mapping method on the mapper which is implemented by the user.
diff --git a/src/Riok.Mapperly/Descriptors/MemberPathCandidateBuilder.cs b/src/Riok.Mapperly/Descriptors/MemberPathCandidateBuilder.cs
new file mode 100644
index 0000000000..39d864ffee
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/MemberPathCandidateBuilder.cs
@@ -0,0 +1,22 @@
+using Riok.Mapperly.Helpers;
+
+namespace Riok.Mapperly.Descriptors;
+
+public static class MemberPathCandidateBuilder
+{
+ internal static IEnumerable> BuildMemberPathCandidates(string name)
+ {
+ var chunks = StringChunker.ChunkPascalCase(name).ToList();
+ for (var i = 1 << chunks.Count - 1; i > 0; i--)
+ {
+ yield return BuildName(chunks, i);
+ }
+ }
+
+ private static IEnumerable BuildName(IEnumerable chunks, int splitPositions)
+ {
+ return chunks
+ .Chunk((_, i) => (splitPositions & (1 << i)) == 0)
+ .Select(x => string.Concat(x));
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/MethodNameBuilder.cs b/src/Riok.Mapperly/Descriptors/MethodNameBuilder.cs
index 393abf2cd2..53bf05d5ce 100644
--- a/src/Riok.Mapperly/Descriptors/MethodNameBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MethodNameBuilder.cs
@@ -1,4 +1,4 @@
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
namespace Riok.Mapperly.Descriptors;
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/ObjectPropertyMapping.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/ObjectPropertyMapping.cs
deleted file mode 100644
index 07d88663a0..0000000000
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/ObjectPropertyMapping.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-
-namespace Riok.Mapperly.Descriptors.TypeMappings;
-
-///
-/// Represents a complex object mapping implemented in its own method.
-/// Maps each property from the source to the target.
-///
-public abstract class ObjectPropertyMapping : MethodMapping
-{
- private readonly List _propertyMappings = new();
-
- protected ObjectPropertyMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : base(sourceType, targetType)
- {
- }
-
- public void AddPropertyMapping(PropertyMapping propertyMapping)
- => _propertyMappings.Add(propertyMapping);
-
- internal IEnumerable BuildBody(ExpressionSyntax source, ExpressionSyntax target)
- => _propertyMappings.Select(x => x.Build(source, target));
-}
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/PropertyMapping.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/PropertyMapping.cs
deleted file mode 100644
index f7c5965024..0000000000
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/PropertyMapping.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System.Diagnostics;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Riok.Mapperly.Helpers;
-using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
-using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
-
-namespace Riok.Mapperly.Descriptors.TypeMappings;
-
-[DebuggerDisplay("PropertyMapping({_source.Name} => {_target.Name})")]
-public class PropertyMapping
-{
- private const string NullableValueProperty = "Value";
-
- private readonly TypeMapping _mapping;
- private readonly IPropertySymbol _source;
- private readonly IPropertySymbol _target;
- private readonly bool _throwInsteadOfConditionalNullMapping;
-
- public PropertyMapping(
- IPropertySymbol source,
- IPropertySymbol target,
- TypeMapping mapping,
- bool throwInsteadOfConditionalNullMapping)
- {
- _source = source;
- _target = target;
- _mapping = mapping;
- _throwInsteadOfConditionalNullMapping = throwInsteadOfConditionalNullMapping;
- }
-
- public StatementSyntax Build(
- ExpressionSyntax sourceAccess,
- ExpressionSyntax targetAccess)
- {
- var targetPropertyAccess = MemberAccess(targetAccess, _target.Name);
- ExpressionSyntax sourcePropertyAccess = MemberAccess(sourceAccess, _source.Name);
-
- // if source is nullable, but mapping doesn't accept nulls
- // condition: source != null
- (var condition, sourcePropertyAccess) = BuildPreMappingCondition(sourcePropertyAccess);
- var mappedValue = _mapping.Build(sourcePropertyAccess);
-
- // target.Property = mappedValue;
- var assignment = AssignmentExpression(
- SyntaxKind.SimpleAssignmentExpression,
- targetPropertyAccess,
- mappedValue);
- var assignmentExpression = ExpressionStatement(assignment);
-
- // if (source.Value != null)
- // target.Value = Map(Source.Name);
- // else
- // throw ...
- return BuildIf(condition, assignmentExpression, sourcePropertyAccess);
- }
-
- private StatementSyntax BuildIf(ExpressionSyntax? condition, StatementSyntax assignment, ExpressionSyntax sourcePropertyAccess)
- {
- if (condition == null)
- return assignment;
-
- var elseClause = _throwInsteadOfConditionalNullMapping
- ? ElseClause(ExpressionStatement(ThrowNewArgumentNullException(sourcePropertyAccess)))
- : null;
- return IfStatement(condition, assignment, elseClause);
- }
-
- private (ExpressionSyntax? Condition, ExpressionSyntax SourceAccess) BuildPreMappingCondition(ExpressionSyntax sourceAccess)
- {
- if (!_source.IsNullable() || _mapping.SourceType.IsNullable() || (_mapping is DirectAssignmentMapping && _target.IsNullable()))
- return (null, sourceAccess);
-
- // if source is nullable but the mapping does not accept nulls
- // and is also not a direct assignment where the target is also nullable
- // add not null condition
- var condition = IsNotNull(sourceAccess);
-
- // source != null
- // if the source is a nullable value type
- // replace source by source.Value for the mapping
- if (_source.Type.IsNullableValueType())
- {
- sourceAccess = MemberAccess(sourceAccess, NullableValueProperty);
- }
-
- return (condition, sourceAccess);
- }
-}
diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
index 601cdcff17..1ec05e550c 100644
--- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
+++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
@@ -67,4 +67,36 @@ internal static class DiagnosticDescriptors
DiagnosticCategories.Mapper,
DiagnosticSeverity.Error,
true);
+
+ public static readonly DiagnosticDescriptor CanNotMapToReadOnlyProperty = new(
+ "RMG009",
+ "Can not map to read only property",
+ "Can not map property {0}.{1} of type {2} to read only property {3}.{4} of type {5}",
+ DiagnosticCategories.Mapper,
+ DiagnosticSeverity.Info,
+ true);
+
+ public static readonly DiagnosticDescriptor CanNotMapFromWriteOnlyProperty = new(
+ "RMG010",
+ "Can not map from write only property",
+ "Can not map from write only property {0}.{1} of type {2} to property {3}.{4} of type {5}",
+ DiagnosticCategories.Mapper,
+ DiagnosticSeverity.Info,
+ true);
+
+ public static readonly DiagnosticDescriptor CanNotMapToWriteOnlyPropertyPath = new(
+ "RMG011",
+ "Can not map to write only property path",
+ "Can not map from property {0}.{1} of type {2} to write only property path {3}.{4} of type {5}",
+ DiagnosticCategories.Mapper,
+ DiagnosticSeverity.Info,
+ true);
+
+ public static readonly DiagnosticDescriptor MappingSourcePropertyNotFound = new(
+ "RMG012",
+ "Mapping source property not found",
+ "Property {0} on source type {1} was not found",
+ DiagnosticCategories.Mapper,
+ DiagnosticSeverity.Info,
+ true);
}
diff --git a/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs
index 97d7928451..ce21b2c002 100644
--- a/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs
+++ b/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs
@@ -1,7 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
@@ -17,6 +17,8 @@ public static class SyntaxFactoryHelper
public static readonly IdentifierNameSyntax VarIdentifier = IdentifierName("var");
+ private static readonly IdentifierNameSyntax _nameofIdentifier = IdentifierName("nameof");
+
public static SyntaxToken Accessibility(Accessibility accessibility)
{
return accessibility switch
@@ -93,8 +95,11 @@ public static MemberAccessExpressionSyntax MemberAccess(string identifierName, s
public static MemberAccessExpressionSyntax MemberAccess(ExpressionSyntax idExpression, string propertyIdentifierName)
=> MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, idExpression, IdentifierName(propertyIdentifierName));
+ public static ConditionalAccessExpressionSyntax ConditionalAccess(ExpressionSyntax idExpression, string propertyIdentifierName)
+ => ConditionalAccessExpression(idExpression, MemberBindingExpression(IdentifierName(propertyIdentifierName)));
+
public static InvocationExpressionSyntax NameOf(ExpressionSyntax expression)
- => Invocation(IdentifierName("nameof"), expression);
+ => Invocation(_nameofIdentifier, expression);
public static ThrowExpressionSyntax ThrowArgumentOutOfRangeException(ExpressionSyntax arg)
{
diff --git a/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs b/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs
index 399391c9c4..23276911b4 100644
--- a/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs
+++ b/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs
@@ -28,9 +28,54 @@ public static HashSet ToHashSet(this IEnumerable enumerable)
}
}
- public static IEnumerable WhereNotNull(this IEnumerable enumerable)
- where T : class
-#nullable disable
- => enumerable.Where(x => x != null);
-#nullable enable
+ public static IEnumerable> Chunk(this IEnumerable enumerable, Func shouldChunk)
+ {
+ var l = new List();
+ var i = 0;
+ foreach (var item in enumerable)
+ {
+ l.Add(item);
+ if (!shouldChunk(item, i++))
+ continue;
+
+ if (l.Count == 0)
+ continue;
+
+ yield return l;
+ l = new();
+ }
+
+ if (l.Count != 0)
+ yield return l;
+ }
+
+ public static IEnumerable SkipLast(this IEnumerable enumerable)
+ {
+ using var enumerator = enumerable.GetEnumerator();
+ if (!enumerator.MoveNext())
+ yield break;
+
+ var previousItem = enumerator.Current;
+ while (enumerator.MoveNext())
+ {
+ yield return previousItem;
+ previousItem = enumerator.Current;
+ }
+ }
+
+ public static TAccumulate AggregateWithPrevious(
+ this IEnumerable source,
+ TAccumulate seed,
+ Func func)
+ {
+ var result = seed;
+ T? prev = default;
+ foreach (var element in source)
+ {
+ result = func(result, prev, element);
+ prev = element;
+ }
+
+ return result;
+ }
}
diff --git a/src/Riok.Mapperly/Helpers/StringChunker.cs b/src/Riok.Mapperly/Helpers/StringChunker.cs
new file mode 100644
index 0000000000..5457f8103e
--- /dev/null
+++ b/src/Riok.Mapperly/Helpers/StringChunker.cs
@@ -0,0 +1,30 @@
+using System.Text;
+
+namespace Riok.Mapperly.Helpers;
+
+public static class StringChunker
+{
+ internal static IEnumerable ChunkPascalCase(string str)
+ {
+ var sb = new StringBuilder();
+ foreach (var c in str)
+ {
+ if (!char.IsUpper(c))
+ {
+ sb.Append(c);
+ continue;
+ }
+
+ if (sb.Length != 0)
+ {
+ yield return sb.ToString();
+ sb.Clear();
+ }
+
+ sb.Append(c);
+ }
+
+ if (sb.Length != 0)
+ yield return sb.ToString();
+ }
+}
diff --git a/src/Riok.Mapperly/Riok.Mapperly.csproj b/src/Riok.Mapperly/Riok.Mapperly.csproj
index e58fd4704b..76820963bd 100644
--- a/src/Riok.Mapperly/Riok.Mapperly.csproj
+++ b/src/Riok.Mapperly/Riok.Mapperly.csproj
@@ -22,7 +22,7 @@
-
+
diff --git a/test/Riok.Mapperly.Abstractions.Test/MapPropertyAttributeTest.cs b/test/Riok.Mapperly.Abstractions.Test/MapPropertyAttributeTest.cs
new file mode 100644
index 0000000000..c67a723508
--- /dev/null
+++ b/test/Riok.Mapperly.Abstractions.Test/MapPropertyAttributeTest.cs
@@ -0,0 +1,14 @@
+namespace Riok.Mapperly.Abstractions.Test;
+
+public class MapPropertyAttributeTest
+{
+ [Fact]
+ public void ShouldSplitMemberAccess()
+ {
+ var attr = new MapPropertyAttribute("a.b.c", "d.e.f");
+ attr.Source.Should().BeEquivalentTo("a", "b", "c");
+ attr.SourceFullName.Should().BeEquivalentTo("a.b.c");
+ attr.Target.Should().BeEquivalentTo("d", "e", "f");
+ attr.TargetFullName.Should().BeEquivalentTo("d.e.f");
+ }
+}
diff --git a/test/Riok.Mapperly.Abstractions.Test/Riok.Mapperly.Abstractions.Test.csproj b/test/Riok.Mapperly.Abstractions.Test/Riok.Mapperly.Abstractions.Test.csproj
new file mode 100644
index 0000000000..80ceab9d51
--- /dev/null
+++ b/test/Riok.Mapperly.Abstractions.Test/Riok.Mapperly.Abstractions.Test.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/IdObjectDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/IdObjectDto.cs
new file mode 100644
index 0000000000..2f98ccc381
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/Dto/IdObjectDto.cs
@@ -0,0 +1,6 @@
+namespace Riok.Mapperly.IntegrationTests.Dto;
+
+public class IdObjectDto
+{
+ public int IdValue { get; set; }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
index 603d8eebeb..5ae9789a06 100644
--- a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
@@ -10,6 +10,16 @@ public class TestObjectDto
public string RenamedStringValue2 { get; set; } = string.Empty;
+ public int FlatteningIdValue { get; set; }
+
+ public int? NullableFlatteningIdValue { get; set; }
+
+ public IdObjectDto Unflattening { get; set; } = new();
+
+ public IdObjectDto? NullableUnflattening { get; set; }
+
+ public int NestedNullableIntValue { get; set; }
+
public TestObjectNestedDto? NestedNullable { get; set; }
public TestObjectNestedDto NestedNullableTargetNotNullable { get; set; } = new();
diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
index 1bf99f6da1..f2a776fb93 100644
--- a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
@@ -32,6 +32,12 @@ public TestObjectDto MapToDto(TestObject src)
[MapperIgnore(nameof(TestObjectDto.IgnoredStringValue))]
[MapProperty(nameof(TestObject.RenamedStringValue), nameof(TestObjectDto.RenamedStringValue2))]
+ [MapProperty(
+ new[] { nameof(TestObject.UnflatteningIdValue) },
+ new[] { nameof(TestObjectDto.Unflattening), nameof(TestObjectDto.Unflattening.IdValue) })]
+ [MapProperty(
+ nameof(TestObject.NullableUnflatteningIdValue),
+ $"{nameof(TestObjectDto.NullableUnflattening)}.{nameof(TestObjectDto.NullableUnflattening.IdValue)}")]
private partial TestObjectDto MapToDtoInternal(TestObject testObject);
[MapperIgnore(nameof(TestObject.IgnoredStringValue))]
diff --git a/test/Riok.Mapperly.IntegrationTests/MapperTest.cs b/test/Riok.Mapperly.IntegrationTests/MapperTest.cs
index f0b43849ef..d06f6988f7 100644
--- a/test/Riok.Mapperly.IntegrationTests/MapperTest.cs
+++ b/test/Riok.Mapperly.IntegrationTests/MapperTest.cs
@@ -51,6 +51,10 @@ private TestObject NewTestObj()
StringNullableTargetNotNullable = "fooBar3",
EnumReverseStringValue = nameof(TestEnumDtoByValue.DtoValue3),
NestedNullableTargetNotNullable = new(),
+ Flattening = new() { IdValue = 10 },
+ NullableFlattening = new() { IdValue = 100 },
+ UnflatteningIdValue = 20,
+ NullableUnflatteningIdValue = 200,
RecursiveObject =
new()
{
diff --git a/test/Riok.Mapperly.IntegrationTests/Models/IdObject.cs b/test/Riok.Mapperly.IntegrationTests/Models/IdObject.cs
new file mode 100644
index 0000000000..01e17354f7
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/Models/IdObject.cs
@@ -0,0 +1,6 @@
+namespace Riok.Mapperly.IntegrationTests.Models;
+
+public class IdObject
+{
+ public int IdValue { get; set; }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
index 777266f8a4..a233f94460 100644
--- a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
@@ -8,6 +8,14 @@ public class TestObject
public string RenamedStringValue { get; set; } = string.Empty;
+ public IdObject Flattening { get; set; } = new();
+
+ public IdObject? NullableFlattening { get; set; }
+
+ public int UnflatteningIdValue { get; set; }
+
+ public int? NullableUnflatteningIdValue { get; set; }
+
public TestObjectNested? NestedNullable { get; set; }
public TestObjectNested? NestedNullableTargetNotNullable { get; set; }
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork.verified.txt
index b4901c516a..cda9c7c0a1 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork.verified.txt
@@ -2,6 +2,15 @@
IntValue: 10,
StringValue: fooBar+after-map,
RenamedStringValue2: fooBar2,
+ FlatteningIdValue: 10,
+ NullableFlatteningIdValue: 100,
+ Unflattening: {
+ IdValue: 20
+ },
+ NullableUnflattening: {
+ IdValue: 200
+ },
+ NestedNullableIntValue: 100,
NestedNullable: {
IntValue: 100
},
@@ -10,6 +19,7 @@
RecursiveObject: {
StringValue: +after-map,
RenamedStringValue2: ,
+ Unflattening: {},
NestedNullableTargetNotNullable: {},
StringNullableTargetNotNullable: ,
EnumValue: DtoValue1,
@@ -21,6 +31,7 @@
IntValue: 99,
StringValue: ,
RenamedStringValue: ,
+ Flattening: {},
EnumReverseStringValue:
},
NullableReadOnlyObjectCollection: [
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs
index 0afe1cbdd7..173ea7ea63 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs
@@ -48,22 +48,40 @@ public partial class TestMapper
var target = new Riok.Mapperly.IntegrationTests.Models.TestObject();
target.IntValue = DirectInt(dto.IntValue);
target.StringValue = dto.StringValue;
+ target.UnflatteningIdValue = DirectInt(dto.Unflattening.IdValue);
+ if (dto.NullableUnflattening != null)
+ {
+ target.NullableUnflatteningIdValue = CastIntNullable(dto.NullableUnflattening.IdValue);
+ }
+
if (dto.NestedNullable != null)
+ {
target.NestedNullable = MapToTestObjectNested(dto.NestedNullable);
+ }
+
target.NestedNullableTargetNotNullable = MapToTestObjectNested(dto.NestedNullableTargetNotNullable);
target.StringNullableTargetNotNullable = dto.StringNullableTargetNotNullable;
if (dto.RecursiveObject != null)
+ {
target.RecursiveObject = MapFromDto(dto.RecursiveObject);
+ }
+
target.SourceTargetSameObjectType = dto.SourceTargetSameObjectType;
if (dto.NullableReadOnlyObjectCollection != null)
+ {
target.NullableReadOnlyObjectCollection = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(dto.NullableReadOnlyObjectCollection, x => MapToTestObjectNested(x)));
+ }
+
target.EnumValue = (Riok.Mapperly.IntegrationTests.Models.TestEnum)dto.EnumValue;
target.EnumName = (Riok.Mapperly.IntegrationTests.Models.TestEnum)dto.EnumName;
target.EnumRawValue = (Riok.Mapperly.IntegrationTests.Models.TestEnum)dto.EnumRawValue;
target.EnumStringValue = MapToTestEnum(dto.EnumStringValue);
target.EnumReverseStringValue = MapToString1(dto.EnumReverseStringValue);
if (dto.SubObject != null)
+ {
target.SubObject = MapToInheritanceSubObject(dto.SubObject);
+ }
+
return target;
}
@@ -158,24 +176,56 @@ private Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject MapToInherita
target.IntValue = DirectInt(testObject.IntValue);
target.StringValue = testObject.StringValue;
target.RenamedStringValue2 = testObject.RenamedStringValue;
+ target.FlatteningIdValue = DirectInt(testObject.Flattening.IdValue);
+ if (testObject.NullableFlattening != null)
+ {
+ target.NullableFlatteningIdValue = CastIntNullable(testObject.NullableFlattening.IdValue);
+ }
+
+ target.Unflattening.IdValue = DirectInt(testObject.UnflatteningIdValue);
+ if (testObject.NullableUnflatteningIdValue != null)
+ {
+ target.NullableUnflattening ??= new();
+ target.NullableUnflattening.IdValue = DirectInt(testObject.NullableUnflatteningIdValue.Value);
+ }
+
if (testObject.NestedNullable != null)
+ {
+ target.NestedNullableIntValue = DirectInt(testObject.NestedNullable.IntValue);
target.NestedNullable = MapToTestObjectNestedDto(testObject.NestedNullable);
+ }
+
if (testObject.NestedNullableTargetNotNullable != null)
+ {
target.NestedNullableTargetNotNullable = MapToTestObjectNestedDto(testObject.NestedNullableTargetNotNullable);
+ }
+
if (testObject.StringNullableTargetNotNullable != null)
+ {
target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable;
+ }
+
if (testObject.RecursiveObject != null)
+ {
target.RecursiveObject = MapToDto(testObject.RecursiveObject);
+ }
+
target.SourceTargetSameObjectType = testObject.SourceTargetSameObjectType;
if (testObject.NullableReadOnlyObjectCollection != null)
+ {
target.NullableReadOnlyObjectCollection = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(testObject.NullableReadOnlyObjectCollection, x => MapToTestObjectNestedDto(x)));
+ }
+
target.EnumValue = (Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)testObject.EnumValue;
target.EnumName = MapToEnumDtoByName(testObject.EnumName);
target.EnumRawValue = (byte)testObject.EnumRawValue;
target.EnumStringValue = MapToString(testObject.EnumStringValue);
target.EnumReverseStringValue = MapToTestEnumDtoByValue(testObject.EnumReverseStringValue);
if (testObject.SubObject != null)
+ {
target.SubObject = MapToInheritanceSubObjectDto(testObject.SubObject);
+ }
+
return target;
}
@@ -183,24 +233,49 @@ private Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject MapToInherita
{
target.IntValue = DirectInt(source.IntValue);
target.StringValue = source.StringValue;
+ target.FlatteningIdValue = DirectInt(source.Flattening.IdValue);
+ if (source.NullableFlattening != null)
+ {
+ target.NullableFlatteningIdValue = CastIntNullable(source.NullableFlattening.IdValue);
+ }
+
if (source.NestedNullable != null)
+ {
+ target.NestedNullableIntValue = DirectInt(source.NestedNullable.IntValue);
target.NestedNullable = MapToTestObjectNestedDto(source.NestedNullable);
+ }
+
if (source.NestedNullableTargetNotNullable != null)
+ {
target.NestedNullableTargetNotNullable = MapToTestObjectNestedDto(source.NestedNullableTargetNotNullable);
+ }
+
if (source.StringNullableTargetNotNullable != null)
+ {
target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable;
+ }
+
if (source.RecursiveObject != null)
+ {
target.RecursiveObject = MapToDto(source.RecursiveObject);
+ }
+
target.SourceTargetSameObjectType = source.SourceTargetSameObjectType;
if (source.NullableReadOnlyObjectCollection != null)
+ {
target.NullableReadOnlyObjectCollection = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(source.NullableReadOnlyObjectCollection, x => MapToTestObjectNestedDto(x)));
+ }
+
target.EnumValue = (Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)source.EnumValue;
target.EnumName = MapToEnumDtoByName(source.EnumName);
target.EnumRawValue = (byte)source.EnumRawValue;
target.EnumStringValue = MapToString(source.EnumStringValue);
target.EnumReverseStringValue = MapToTestEnumDtoByValue(source.EnumReverseStringValue);
if (source.SubObject != null)
+ {
target.SubObject = MapToInheritanceSubObjectDto(source.SubObject);
+ }
+
target.IgnoredStringValue = source.IgnoredStringValue;
}
diff --git a/test/Riok.Mapperly.Tests/Descriptors/MemberPathCandidateBuilderTest.cs b/test/Riok.Mapperly.Tests/Descriptors/MemberPathCandidateBuilderTest.cs
new file mode 100644
index 0000000000..8fa9fb827d
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/Descriptors/MemberPathCandidateBuilderTest.cs
@@ -0,0 +1,18 @@
+using Riok.Mapperly.Descriptors;
+
+namespace Riok.Mapperly.Tests.Descriptors;
+
+public class MemberPathCandidateBuilderTest
+{
+ [Theory]
+ [InlineData("Value", new[] { "Value" })]
+ [InlineData("MyValue", new[] { "MyValue", "My.Value" })]
+ [InlineData("MyValueId", new[] { "MyValueId", "My.ValueId", "MyValue.Id", "My.Value.Id" })]
+ public void BuildMemberPathCandidatesShouldWork(string name, string[] chunks)
+ {
+ MemberPathCandidateBuilder.BuildMemberPathCandidates(name)
+ .Select(x => string.Join(".", x))
+ .Should()
+ .BeEquivalentTo(chunks);
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs b/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs
index 60b04a642f..f4a837eaa5 100644
--- a/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs
+++ b/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs
@@ -2,7 +2,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Moq;
using Riok.Mapperly.Descriptors;
-using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Descriptors.Mappings;
namespace Riok.Mapperly.Tests.Descriptors;
diff --git a/test/Riok.Mapperly.Tests/Helpers/DictionaryExtensionsTest.cs b/test/Riok.Mapperly.Tests/Helpers/DictionaryExtensionsTest.cs
new file mode 100644
index 0000000000..d9812682c5
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/Helpers/DictionaryExtensionsTest.cs
@@ -0,0 +1,45 @@
+using Riok.Mapperly.Helpers;
+
+namespace Riok.Mapperly.Tests.Helpers;
+
+// can't use extension methods due to ambiguous method reference.
+// (no support for this method in netstandard2.0)
+public class DictionaryExtensionsTest
+{
+ [Fact]
+ public void RemoveShouldReturnTrueWhenKeyWasRemoved()
+ {
+ var d = new Dictionary { ["a"] = 10, ["b"] = 20, };
+ DictionaryExtensions.Remove(d, "a", out var value).Should().BeTrue();
+ value.Should().Be(10);
+ }
+
+ [Fact]
+ public void RemoveShouldReturnFalseWhenKeyWasNotRemoved()
+ {
+ var d = new Dictionary { ["a"] = 10, ["b"] = 20, };
+ DictionaryExtensions.Remove(d, "c", out var value).Should().BeFalse();
+ value.Should().Be(0);
+ }
+
+ [Fact]
+ public void GetValueOrDefaultShouldReturnValueIfFound()
+ {
+ var d = new Dictionary { ["a"] = 10, ["b"] = 20, };
+ DictionaryExtensions.GetValueOrDefault(d, "a").Should().Be(10);
+ }
+
+ [Fact]
+ public void GetValueOrDefaultShouldReturnDefaultForPrimitiveIfNotFound()
+ {
+ var d = new Dictionary { ["a"] = 10, ["b"] = 20, };
+ DictionaryExtensions.GetValueOrDefault(d, "c").Should().Be(0);
+ }
+
+ [Fact]
+ public void GetValueOrDefaultShouldReturnDefaultForReferenceTypeIfNotFound()
+ {
+ var d = new Dictionary { ["a"] = new(), ["b"] = new(), };
+ DictionaryExtensions.GetValueOrDefault(d, "c").Should().BeNull();
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/Helpers/EnumerableExtensionsTest.cs b/test/Riok.Mapperly.Tests/Helpers/EnumerableExtensionsTest.cs
new file mode 100644
index 0000000000..b8c5766405
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/Helpers/EnumerableExtensionsTest.cs
@@ -0,0 +1,71 @@
+using Riok.Mapperly.Helpers;
+
+namespace Riok.Mapperly.Tests.Helpers;
+
+public class EnumerableExtensionsTest
+{
+ [Fact]
+ public void ToHashSetShouldWork()
+ {
+ var items = new[] { 1, 1, 2, 3, 4, 5, 5 };
+
+ // can't use extension method due to ambiguous method reference.
+ // (no support for this method in netstandard2.0)
+ var hashSet = EnumerableExtensions.ToHashSet(items);
+ hashSet.Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5 });
+ }
+
+ [Fact]
+ public void DistinctByShouldWork()
+ {
+ var items = new[]
+ {
+ ("item10", 10),
+ ("item11", 10),
+ ("item12", 10),
+ ("item20", 20),
+ ("item30", 30),
+ ("item31", 30),
+ };
+
+ items
+ .DistinctBy(x => x.Item2)
+ .Select(x => x.Item1)
+ .Should()
+ .BeEquivalentTo(new[] { "item10", "item20", "item30" });
+ }
+
+ [Fact]
+ public void ChunkShouldWork()
+ {
+ Enumerable
+ .Range(0, 5)
+ .Chunk((_, index) => index % 2 == 1)
+ .Should()
+ .BeEquivalentTo(new[]
+ {
+ new[] { 0, 1 },
+ new[] { 2, 3 },
+ new[] { 4 }
+ });
+ }
+
+ [Fact]
+ public void SkipLastShouldWork()
+ {
+ var items = new[] { 1, 2, 5, 6, 7 };
+ items
+ .SkipLast()
+ .Should()
+ .BeEquivalentTo(items.Take(items.Length - 1));
+ }
+
+ [Fact]
+ public void AggregateWithPrevious()
+ {
+ var items = new[] { 1, 2, 5, 6, 7 };
+ items.AggregateWithPrevious(100, (agg, prev, item) => agg - prev + item)
+ .Should()
+ .Be(107);
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/Helpers/QueueExtensionsTest.cs b/test/Riok.Mapperly.Tests/Helpers/QueueExtensionsTest.cs
new file mode 100644
index 0000000000..ae14f1a6e7
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/Helpers/QueueExtensionsTest.cs
@@ -0,0 +1,31 @@
+using Riok.Mapperly.Helpers;
+
+namespace Riok.Mapperly.Tests.Helpers;
+
+public class QueueExtensionsTest
+{
+ [Fact]
+ public void DequeueAllShouldDequeueAll()
+ {
+ var q = new Queue();
+ q.Enqueue(0);
+ q.Enqueue(1);
+
+ var index = 0;
+ foreach (var item in q.DequeueAll())
+ {
+ item.Should().Be(index++);
+
+ // enqueue during dequeue
+ if (item == 0)
+ {
+ for (var i = 2; i < 10; i++)
+ {
+ q.Enqueue(i);
+ }
+ }
+ }
+
+ index.Should().Be(10);
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/Helpers/StringChunkerTest.cs b/test/Riok.Mapperly.Tests/Helpers/StringChunkerTest.cs
new file mode 100644
index 0000000000..9716d30c91
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/Helpers/StringChunkerTest.cs
@@ -0,0 +1,18 @@
+using Riok.Mapperly.Helpers;
+
+namespace Riok.Mapperly.Tests.Helpers;
+
+public class StringChunkerTest
+{
+ [Theory]
+ [InlineData("camelCase", new[] { "camel", "Case" })]
+ [InlineData("PascalCase", new[] { "Pascal", "Case" })]
+ [InlineData("ABC", new[] { "A", "B", "C" })]
+ [InlineData("abcABC", new[] { "abc", "A", "B", "C" })]
+ public void ChunkPascalCaseShouldWork(string str, string[] expected)
+ {
+ StringChunker.ChunkPascalCase(str)
+ .Should()
+ .BeEquivalentTo(expected);
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs
index 22a8780c3f..85e063893a 100644
--- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs
@@ -105,7 +105,10 @@ public void NullableIntToNonNullableIntProperty()
.Should()
.Be(@"var target = new B();
if (source.Value != null)
+ {
target.Value = source.Value.Value;
+ }
+
return target;".ReplaceLineEndings());
}
@@ -122,7 +125,10 @@ public void NullableStringToNonNullableStringProperty()
.Should()
.Be(@"var target = new B();
if (source.Value != null)
+ {
target.Value = source.Value;
+ }
+
return target;".ReplaceLineEndings());
}
@@ -141,7 +147,10 @@ public void NullableClassToNonNullableClassProperty()
.Should()
.Be(@"var target = new B();
if (source.Value != null)
+ {
target.Value = MapToD(source.Value);
+ }
+
return target;".ReplaceLineEndings());
}
@@ -194,7 +203,10 @@ public void DisabledNullableClassPropertyToNonNullableProperty()
.Should()
.Be(@"var target = new B();
if (source.Value != null)
+ {
target.Value = MapToD(source.Value);
+ }
+
return target;".ReplaceLineEndings());
}
@@ -213,7 +225,10 @@ public void NullableClassPropertyToDisabledNullableProperty()
.Should()
.Be(@"var target = new B();
if (source.Value != null)
+ {
target.Value = MapToD(source.Value);
+ }
+
return target;".ReplaceLineEndings());
}
@@ -233,14 +248,19 @@ public void NullableClassToNonNullableClassPropertyThrow()
.Should()
.Be(@"var target = new B();
if (source.Value != null)
+ {
target.Value = MapToD(source.Value);
+ }
else
+ {
throw new System.ArgumentNullException(nameof(source.Value));
+ }
+
return target;".ReplaceLineEndings());
}
[Fact]
- public void ShouldIgnoreWriteOnlyPropertyOnSource()
+ public Task ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics()
{
var source = TestSourceBuilder.Mapping(
"A",
@@ -248,15 +268,11 @@ public void ShouldIgnoreWriteOnlyPropertyOnSource()
"class A { public string StringValue { get; set; } public string StringValue2 { set; } }",
"class B { public string StringValue { get; set; } public string StringValue2 { get; set; } }");
- TestHelper.GenerateSingleMapperMethodBody(source)
- .Should()
- .Be(@"var target = new B();
- target.StringValue = source.StringValue;
- return target;".ReplaceLineEndings());
+ return TestHelper.VerifyGenerator(source);
}
[Fact]
- public void ShouldIgnoreReadOnlyPropertyOnTarget()
+ public Task ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic()
{
var source = TestSourceBuilder.Mapping(
"A",
@@ -264,15 +280,11 @@ public void ShouldIgnoreReadOnlyPropertyOnTarget()
"class A { public string StringValue { get; set; } public string StringValue2 { get; set; } }",
"class B { public string StringValue { get; set; } public string StringValue2 { get; } }");
- TestHelper.GenerateSingleMapperMethodBody(source)
- .Should()
- .Be(@"var target = new B();
- target.StringValue = source.StringValue;
- return target;".ReplaceLineEndings());
+ return TestHelper.VerifyGenerator(source);
}
[Fact]
- public void WithUnmatchedProperty()
+ public Task WithUnmatchedPropertyShouldDiagnostic()
{
var source = TestSourceBuilder.Mapping(
"A",
@@ -280,11 +292,7 @@ public void WithUnmatchedProperty()
"class A { public string StringValue { get; set; } public string StringValueA { get; set; } }",
"class B { public string StringValue { get; set; } public string StringValueB { get; set; } }");
- TestHelper.GenerateSingleMapperMethodBody(source)
- .Should()
- .Be(@"var target = new B();
- target.StringValue = source.StringValue;
- return target;".ReplaceLineEndings());
+ return TestHelper.VerifyGenerator(source);
}
[Fact]
@@ -317,6 +325,28 @@ public void WithManualMappedProperty()
return target;".ReplaceLineEndings());
}
+ [Fact]
+ public Task WithManualMappedNotFoundTargetPropertyShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapProperty(nameof(A.StringValue), nameof(B.StringValue9)] partial B Map(A source);",
+ "class A { public string StringValue { get; set; } }",
+ "class B { public string StringValue2 { get; set; } }");
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task WithManualMappedNotFoundSourcePropertyShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapProperty(nameof(A.StringValue9), nameof(B.StringValue2)] partial B Map(A source);",
+ "class A { public string StringValue { get; set; } }",
+ "class B { public string StringValue2 { get; set; } }");
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
[Fact]
public void ShouldUseUserImplementedMapping()
{
@@ -411,9 +441,178 @@ public Task WithNotFoundIgnoredPropertyShouldDiagnostic()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[MapperIgnore(\"not_found\")] partial B Map(A source);",
- "class A { public string StringValue { get; set; } }",
- "class B { public string StringValue2 { get; set; } }");
+ "class A { }",
+ "class B { }");
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public void ManualFlattenedProperty()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapProperty($\"Value.Id\", \"MyValueId\")] partial B Map(A source);",
+ "class A { public C Value { get; set; } }",
+ "class B { public string MyValueId { get; set; } }",
+ "class C { public string Id { get; set; }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be(@"var target = new B();
+ target.MyValueId = source.Value.Id;
+ return target;".ReplaceLineEndings());
+ }
+
+ [Fact]
+ public void AutoFlattenedProperty()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ "class A { public C Value { get; set; } }",
+ "class B { public string ValueId { get; set; } }",
+ "class C { public string Id { get; set; }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be(@"var target = new B();
+ target.ValueId = source.Value.Id;
+ return target;".ReplaceLineEndings());
+ }
+
+ [Fact]
+ public void AutoFlattenedPropertyNullablePath()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ "class A { public C? Value { get; set; } }",
+ "class B { public string ValueId { get; set; } }",
+ "class C { public string Id { get; set; }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be(@"var target = new B();
+ if (source.Value != null)
+ {
+ target.ValueId = source.Value.Id;
+ }
+
+ return target;".ReplaceLineEndings());
+ }
+
+ [Fact]
+ public void ManualUnflattenedProperty()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapProperty($\"MyValueId\", \"Value.Id\")] partial B Map(A source);",
+ "class A { public string MyValueId { get; set; } }",
+ "class B { public C Value { get; set; } }",
+ "class C { public string Id { get; set; }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be(@"var target = new B();
+ target.Value.Id = source.MyValueId;
+ return target;".ReplaceLineEndings());
+ }
+
+ [Fact]
+ public void ManualUnflattenedPropertyNullablePath()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapProperty($\"MyValueId\", \"Value.Id\"), MapProperty($\"MyValueId2\", \"Value.Id2\")] partial B Map(A source);",
+ "class A { public string MyValueId { get; set; } public string MyValueId2 { get; set; } }",
+ "class B { public C? Value { get; set; } }",
+ "class C { public string Id { get; set; } public string Id2 { get; set; } }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be(@"var target = new B();
+ target.Value ??= new();
+ target.Value.Id = source.MyValueId;
+ target.Value.Id2 = source.MyValueId2;
+ return target;".ReplaceLineEndings());
+ }
+
+ [Fact]
+ public Task ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapProperty($\"MyValueId\", \"Value.Id\")] partial B Map(A source);",
+ "class A { public string MyValueId { get; set; } }",
+ "class B { public C? Value { get; set; } }",
+ "class C { public C(string arg) {} public string Id { get; set; } }");
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapProperty($\"MyValueIdXXX\", \"Value.Id\")] partial B Map(A source);",
+ "class A { public string MyValueId { get; set; } }",
+ "class B { public C? Value { get; set; } }",
+ "class C { public C(string arg) {} public string Id { get; set; } }");
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapProperty($\"MyValueId\", \"Value.Id\")] partial B Map(A source);",
+ "class A { public string MyValueId { get; set; } }",
+ "class B { public C? Value { set; } }",
+ "class C { public C(string arg) {} public string Id { get; set; } }");
return TestHelper.VerifyGenerator(source);
}
+
+ [Fact]
+ public Task ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapProperty($\"MyValueId\", \"Value.IdXXX\")] partial B Map(A source);",
+ "class A { public string MyValueId { get; set; } }",
+ "class B { public C? Value { get; set; } }",
+ "class C { public C(string arg) {} public string Id { get; set; } }");
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public void ManualNestedPropertyNullablePath()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapProperty(\"Value1.Value1.Id1\", \"Value2.Value2.Id2\")]" +
+ "[MapProperty(\"Value1.Value1.Id10\", \"Value2.Value2.Id20\")]" +
+ "[MapProperty(new[] { \"Value1\", \"Id100\" }, new[] { \"Value2\", \"Id200\" })]" +
+ "partial B Map(A source);",
+ "class A { public C? Value1 { get; set; } }",
+ "class B { public E? Value2 { get; set; } }",
+ "class C { public D? Value1 { get; set; } public string Id100 { get; set; } }",
+ "class D { public string Id1 { get; set; } public string Id10 { get; set; } }",
+ "class E { public F? Value2 { get; set; } public string Id200 { get; set; } }",
+ "class F { public string Id2 { get; set; } public string Id20 { get; set; } }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be(@"var target = new B();
+ if (source.Value1 != null)
+ {
+ target.Value2 ??= new();
+ target.Value2.Id200 = source.Value1.Id100;
+ if (source.Value1?.Value1 != null)
+ {
+ target.Value2.Value2 ??= new();
+ target.Value2.Value2.Id2 = source.Value1.Value1.Id1;
+ target.Value2.Value2.Id20 = source.Value1.Value1.Id10;
+ }
+ }
+
+ return target;".ReplaceLineEndings());
+ }
}
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.00.verified.txt
new file mode 100644
index 0000000000..f101ba9364
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.00.verified.txt
@@ -0,0 +1,17 @@
+{
+ Diagnostics: [
+ {
+ Id: RMG002,
+ Title: No accessible parameterless constructor found,
+ Severity: Error,
+ WarningLevel: 0,
+ Location: : (10,4)-(10,68),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: {0} has no accessible parameterless constructor,
+ Message: C? has no accessible parameterless constructor,
+ Category: Mapper,
+ CustomTags: []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.01.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.01.verified.cs
new file mode 100644
index 0000000000..555a081839
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.01.verified.cs
@@ -0,0 +1,11 @@
+//HintName: Mapper.g.cs
+#nullable enable
+public partial class Mapper
+{
+ private partial B Map(A source)
+ {
+ var target = new B();
+ target.Value.Id = source.MyValueId;
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.00.verified.txt
new file mode 100644
index 0000000000..d20d9c4861
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.00.verified.txt
@@ -0,0 +1,17 @@
+{
+ Diagnostics: [
+ {
+ Id: RMG006,
+ Title: Mapping source property not found,
+ Severity: Error,
+ WarningLevel: 0,
+ Location: : (10,4)-(10,71),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: Specified property {0} on source type {1} was not found,
+ Message: Specified property MyValueIdXXX on source type A was not found,
+ Category: Mapper,
+ CustomTags: []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.01.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.01.verified.cs
new file mode 100644
index 0000000000..3e34a4c677
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.01.verified.cs
@@ -0,0 +1,10 @@
+//HintName: Mapper.g.cs
+#nullable enable
+public partial class Mapper
+{
+ private partial B Map(A source)
+ {
+ var target = new B();
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.00.verified.txt
new file mode 100644
index 0000000000..73ad2f6cf6
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.00.verified.txt
@@ -0,0 +1,17 @@
+{
+ Diagnostics: [
+ {
+ Id: RMG005,
+ Title: Mapping target property not found,
+ Severity: Error,
+ WarningLevel: 0,
+ Location: : (10,4)-(10,71),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: Specified property {0} on mapping target type {1} was not found,
+ Message: Specified property Value.IdXXX on mapping target type B was not found,
+ Category: Mapper,
+ CustomTags: []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.01.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.01.verified.cs
new file mode 100644
index 0000000000..3e34a4c677
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.01.verified.cs
@@ -0,0 +1,10 @@
+//HintName: Mapper.g.cs
+#nullable enable
+public partial class Mapper
+{
+ private partial B Map(A source)
+ {
+ var target = new B();
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.00.verified.txt
new file mode 100644
index 0000000000..a37b3e7602
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.00.verified.txt
@@ -0,0 +1,17 @@
+{
+ Diagnostics: [
+ {
+ Id: RMG011,
+ Title: Can not map to write only property path,
+ Severity: Info,
+ WarningLevel: 1,
+ Location: : (10,4)-(10,68),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: Can not map from property {0}.{1} of type {2} to write only property path {3}.{4} of type {5},
+ Message: Can not map from property A.MyValueId of type string to write only property path B.Value.Id of type string,
+ Category: Mapper,
+ CustomTags: []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.01.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.01.verified.cs
new file mode 100644
index 0000000000..3e34a4c677
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.01.verified.cs
@@ -0,0 +1,10 @@
+//HintName: Mapper.g.cs
+#nullable enable
+public partial class Mapper
+{
+ private partial B Map(A source)
+ {
+ var target = new B();
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.00.verified.txt
new file mode 100644
index 0000000000..55f9b7b518
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.00.verified.txt
@@ -0,0 +1,17 @@
+{
+ Diagnostics: [
+ {
+ Id: RMG009,
+ Title: Can not map to read only property,
+ Severity: Info,
+ WarningLevel: 1,
+ Location: : (10,4)-(10,28),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: Can not map property {0}.{1} of type {2} to read only property {3}.{4} of type {5},
+ Message: Can not map property A.StringValue2 of type string to read only property B.StringValue2 of type string,
+ Category: Mapper,
+ CustomTags: []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.01.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.01.verified.cs
new file mode 100644
index 0000000000..83d6844209
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.01.verified.cs
@@ -0,0 +1,11 @@
+//HintName: Mapper.g.cs
+#nullable enable
+public partial class Mapper
+{
+ private partial B Map(A source)
+ {
+ var target = new B();
+ target.StringValue = source.StringValue;
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.00.verified.txt
new file mode 100644
index 0000000000..acb840306a
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.00.verified.txt
@@ -0,0 +1,17 @@
+{
+ Diagnostics: [
+ {
+ Id: RMG010,
+ Title: Can not map from write only property,
+ Severity: Info,
+ WarningLevel: 1,
+ Location: : (10,4)-(10,28),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: Can not map from write only property {0}.{1} of type {2} to property {3}.{4} of type {5},
+ Message: Can not map from write only property A.StringValue2 of type string to property B.StringValue2 of type string,
+ Category: Mapper,
+ CustomTags: []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.01.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.01.verified.cs
new file mode 100644
index 0000000000..83d6844209
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.01.verified.cs
@@ -0,0 +1,11 @@
+//HintName: Mapper.g.cs
+#nullable enable
+public partial class Mapper
+{
+ private partial B Map(A source)
+ {
+ var target = new B();
+ target.StringValue = source.StringValue;
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.00.verified.txt
new file mode 100644
index 0000000000..1464392f67
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.00.verified.txt
@@ -0,0 +1,17 @@
+{
+ Diagnostics: [
+ {
+ Id: RMG006,
+ Title: Mapping source property not found,
+ Severity: Error,
+ WarningLevel: 0,
+ Location: : (10,4)-(10,89),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: Specified property {0} on source type {1} was not found,
+ Message: Specified property StringValue9 on source type A was not found,
+ Category: Mapper,
+ CustomTags: []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.01.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.01.verified.cs
new file mode 100644
index 0000000000..3e34a4c677
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.01.verified.cs
@@ -0,0 +1,10 @@
+//HintName: Mapper.g.cs
+#nullable enable
+public partial class Mapper
+{
+ private partial B Map(A source)
+ {
+ var target = new B();
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.00.verified.txt
new file mode 100644
index 0000000000..6ee8d34de0
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.00.verified.txt
@@ -0,0 +1,30 @@
+{
+ Diagnostics: [
+ {
+ Id: RMG012,
+ Title: Mapping source property not found,
+ Severity: Info,
+ WarningLevel: 1,
+ Location: : (10,4)-(10,88),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: Property {0} on source type {1} was not found,
+ Message: Property StringValue2 on source type A was not found,
+ Category: Mapper,
+ CustomTags: []
+ },
+ {
+ Id: RMG005,
+ Title: Mapping target property not found,
+ Severity: Error,
+ WarningLevel: 0,
+ Location: : (10,4)-(10,88),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: Specified property {0} on mapping target type {1} was not found,
+ Message: Specified property StringValue9 on mapping target type B was not found,
+ Category: Mapper,
+ CustomTags: []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.01.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.01.verified.cs
new file mode 100644
index 0000000000..3e34a4c677
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.01.verified.cs
@@ -0,0 +1,10 @@
+//HintName: Mapper.g.cs
+#nullable enable
+public partial class Mapper
+{
+ private partial B Map(A source)
+ {
+ var target = new B();
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.00.verified.txt
index 003243441c..f51e1498f1 100644
--- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.00.verified.txt
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.00.verified.txt
@@ -9,7 +9,7 @@
Description: ,
HelpLink: ,
MessageFormat: Specified property {0} on source type {1} was not found,
- Message: Specified property not_found on source type B was not found,
+ Message: Specified property not_found on source type A was not found,
Category: Mapper,
CustomTags: []
}
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.00.verified.txt
new file mode 100644
index 0000000000..081f024cef
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.00.verified.txt
@@ -0,0 +1,17 @@
+{
+ Diagnostics: [
+ {
+ Id: RMG012,
+ Title: Mapping source property not found,
+ Severity: Info,
+ WarningLevel: 1,
+ Location: : (10,4)-(10,28),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: Property {0} on source type {1} was not found,
+ Message: Property StringValueB on source type A was not found,
+ Category: Mapper,
+ CustomTags: []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.01.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.01.verified.cs
new file mode 100644
index 0000000000..83d6844209
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.01.verified.cs
@@ -0,0 +1,11 @@
+//HintName: Mapper.g.cs
+#nullable enable
+public partial class Mapper
+{
+ private partial B Map(A source)
+ {
+ var target = new B();
+ target.StringValue = source.StringValue;
+ return target;
+ }
+}
\ No newline at end of file