From ebce68ed5653efc3d8a5df01acd91fdccf21fbfb Mon Sep 17 00:00:00 2001 From: Jaidon Rymer Date: Mon, 11 Mar 2024 12:58:20 +0000 Subject: [PATCH] feat: Add MapperIgnoreMemberAttribute to ignore members at declaration (#1143) * feat: Added a MapperIgnoreMemberAttribute to allow mapping to be ignored from within a source/target class. Works the same as the ObsoleteAttribute without the configuration due to being explicitly specified. --- docs/docs/configuration/mapper.mdx | 26 ++++++ .../MapperIgnoreAttribute.cs | 10 +++ .../PublicAPI.Shipped.txt | 2 + .../MembersMappingBuilderContext.cs | 22 ++++- .../Dto/TestObjectDto.cs | 4 + .../Mapping/IgnoreAttributeTest.cs | 82 +++++++++++++++++++ 6 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 src/Riok.Mapperly.Abstractions/MapperIgnoreAttribute.cs create mode 100644 test/Riok.Mapperly.Tests/Mapping/IgnoreAttributeTest.cs diff --git a/docs/docs/configuration/mapper.mdx b/docs/docs/configuration/mapper.mdx index 558a7bb485..9bd84445aa 100644 --- a/docs/docs/configuration/mapper.mdx +++ b/docs/docs/configuration/mapper.mdx @@ -68,6 +68,32 @@ public partial class CarMapper } ``` +#### Ignore a memeber at definition + +To ignore a property or field at definition, the `MapperIgnoreAttribute` can be used. + +```csharp +public partial class CarMapper +{ + public partial CarDto ToDto(Car car); +} + +public class Car +{ + // highlight-start + [MapperIgnore] + // highlight-end + public int Id { get; set; } + + public string ModelName { get; set; } +} + +public class CarDto +{ + public string ModelName { get; set; } +} +``` + #### Ignore obsolete members By default, Mapperly will map source/target members marked with `ObsoleteAttribute`. diff --git a/src/Riok.Mapperly.Abstractions/MapperIgnoreAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperIgnoreAttribute.cs new file mode 100644 index 0000000000..2834cedbb7 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/MapperIgnoreAttribute.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; + +namespace Riok.Mapperly.Abstractions; + +/// +/// Ignores a member from the mapping. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +[Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] +public sealed class MapperIgnoreAttribute : Attribute; diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index bca69e392d..f1caebc197 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -42,6 +42,8 @@ Riok.Mapperly.Abstractions.MapperConstructorAttribute.MapperConstructorAttribute Riok.Mapperly.Abstractions.MapperIgnoreObsoleteMembersAttribute Riok.Mapperly.Abstractions.MapperIgnoreObsoleteMembersAttribute.IgnoreObsoleteStrategy.get -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy Riok.Mapperly.Abstractions.MapperIgnoreObsoleteMembersAttribute.MapperIgnoreObsoleteMembersAttribute(Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy ignoreObsoleteStrategy = (Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy)-1) -> void +Riok.Mapperly.Abstractions.MapperIgnoreAttribute +Riok.Mapperly.Abstractions.MapperIgnoreAttribute.MapperIgnoreAttribute() -> void Riok.Mapperly.Abstractions.MapperIgnoreSourceAttribute Riok.Mapperly.Abstractions.MapperIgnoreSourceAttribute.MapperIgnoreSourceAttribute(string! source) -> void Riok.Mapperly.Abstractions.MapperIgnoreSourceAttribute.Source.get -> string! diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs index 643e6c4dbf..b05e50da51 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs @@ -34,10 +34,12 @@ protected MembersMappingBuilderContext(MappingBuilderContext builderContext, T m TargetMembers = GetTargetMembers(); IgnoredSourceMemberNames = builderContext - .Configuration.Members.IgnoredSources.Concat(GetIgnoredObsoleteSourceMembers()) + .Configuration.Members.IgnoredSources.Concat(GetIgnoredSourceMembers()) + .Concat(GetIgnoredObsoleteSourceMembers()) .ToHashSet(); var ignoredTargetMemberNames = builderContext - .Configuration.Members.IgnoredTargets.Concat(GetIgnoredObsoleteTargetMembers()) + .Configuration.Members.IgnoredTargets.Concat(GetIgnoredTargetMembers()) + .Concat(GetIgnoredObsoleteTargetMembers()) .ToHashSet(); _ignoredUnmatchedSourceMemberNames = InitIgnoredUnmatchedProperties(IgnoredSourceMemberNames, _unmappedSourceMemberNames); @@ -143,6 +145,22 @@ private IEnumerable GetIgnoredObsoleteSourceMembers() .Select(x => x.Name); } + private IEnumerable GetIgnoredTargetMembers() + { + return BuilderContext + .SymbolAccessor.GetAllAccessibleMappableMembers(Mapping.TargetType) + .Where(x => BuilderContext.SymbolAccessor.HasAttribute(x.MemberSymbol)) + .Select(x => x.Name); + } + + private IEnumerable GetIgnoredSourceMembers() + { + return BuilderContext + .SymbolAccessor.GetAllAccessibleMappableMembers(Mapping.SourceType) + .Where(x => BuilderContext.SymbolAccessor.HasAttribute(x.MemberSymbol)) + .Select(x => x.Name); + } + private HashSet GetSourceMemberNames() { return BuilderContext.SymbolAccessor.GetAllAccessibleMappableMembers(Mapping.SourceType).Select(x => x.Name).ToHashSet(); diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs index 7a391aa5fc..a92c8d2a07 100644 --- a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs +++ b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.IntegrationTests.Models; namespace Riok.Mapperly.IntegrationTests.Dto @@ -119,6 +120,9 @@ public TestObjectDto(int ctorValue, int unknownValue = 10, int ctorValue2 = 100) [Obsolete] public int IgnoredObsoleteValue { get; set; } + [MapperIgnore] + public int IgnoredMemberValue { get; set; } + public DateOnly DateTimeValueTargetDateOnly { get; set; } public TimeOnly DateTimeValueTargetTimeOnly { get; set; } diff --git a/test/Riok.Mapperly.Tests/Mapping/IgnoreAttributeTest.cs b/test/Riok.Mapperly.Tests/Mapping/IgnoreAttributeTest.cs new file mode 100644 index 0000000000..a47e61d79f --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/IgnoreAttributeTest.cs @@ -0,0 +1,82 @@ +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +[UsesVerify] +public class IgnoreAttributeTest +{ + private readonly string _classA = TestSourceBuilder.CSharp( + """ + class A + { + public int Value { get; set; } + + [MapperIgnore] + public int Ignored { get; set; } + } + """ + ); + + private readonly string _classB = TestSourceBuilder.CSharp( + """ + class B + { + public int Value { get; set; } + + [MapperIgnore] + public int Ignored { get; set; } + } + """ + ); + + [Fact] + public void ClassAttributeIgnoreMember() + { + var source = TestSourceBuilder.Mapping("A", "B", TestSourceBuilderOptions.Default, _classA, _classB); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void MapPropertyOverridesIgnoreMember() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("Ignored", "Ignored")] + partial B Map(A source); + """, + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + target.Ignored = source.Ignored; + return target; + """ + ) + .HaveDiagnostic( + DiagnosticDescriptors.IgnoredSourceMemberExplicitlyMapped, + "The source member Ignored on A is ignored, but is also mapped by the MapPropertyAttribute" + ) + .HaveDiagnostic( + DiagnosticDescriptors.IgnoredTargetMemberExplicitlyMapped, + "The target member Ignored on B is ignored, but is also mapped by the MapPropertyAttribute" + ) + .HaveAssertedAllDiagnostics(); + } +}