Skip to content

Commit

Permalink
feat: add IgnoreObsoleteMembers
Browse files Browse the repository at this point in the history
  • Loading branch information
TimothyMakkison committed Jul 14, 2023
1 parent 18dec45 commit 9ad8722
Show file tree
Hide file tree
Showing 20 changed files with 628 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ sidebar_position: 0
description: Define a mapper with Mapperly.
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Mapper configuration

The `MapperAttribute` provides options to customize the generated mapper class.

## Copy behaviour
## Copy behavior

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
Expand Down Expand Up @@ -56,6 +59,51 @@ public partial class CarMapper
}
```

### Ignore obsolete members stratgey

By default, mapperly will map source/target members marked with `ObsoleteAttribute`. This can be changed by setting the `IgnoreObsoleteMembersStrategy` of a method with `MapperIgnoreObsoleteMembersAttribute`, or by setting the `IgnoreObsoleteMembersStrategy` option of the `MapperAttribute`.

| Name | Description |
| ------ | ------------------------------------------------------------------------------- |
| None | Will map members marked with the `Obsolete` attribute (default) |
| Both | Ignores source and target members that are mapped with the `Obsolete` attribute |
| Source | Ignores source members that are mapped with the `Obsolete` attribute |
| Target | Ignores target members that are mapped with the `Obsolete` attribute |

<Tabs>
<TabItem value="global" label="Global (Mapper Level)" default>

Sets the `IgnoreObsoleteMembersStrategy` for all methods within the mapper, by default it is `None` allowing obsolete source and target members to be mapped. This can be overriden by individual mapping methods using `MapperIgnoreObsoleteMembersAttribute`.

```csharp
// highlight-start
[Mapper(IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Both)]
// highlight-end
public partial class CarMapper
{
...
}
```

</TabItem>
<TabItem value="method" label="Local (mapping method level)">

Method will use the provided ignore obsolete mapping strategy, otherwise the `MapperAttribute` property `IgnoreObsoleteMembersStrategy` will be used.

```csharp
[Mapper]
public partial class CarMapper
{
// highlight-start
[MapperIgnoreObsoleteMembers(IgnoreObsoleteMembersStrategy.Both)]
// highlight-end
public partial CarMakeDto MapMake(CarMake make);
}
```

</TabItem>
</Tabs>

### Property name mapping strategy

By default, property and field names are matched using a case sensitive strategy.
Expand Down
30 changes: 30 additions & 0 deletions src/Riok.Mapperly.Abstractions/IgnoreObsoleteMembersStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Defines the strategy to use when mapping members marked with <see cref="ObsoleteAttribute"/>.
/// Note that <see cref="MapPropertyAttribute"/> will always map <see cref="ObsoleteAttribute"/> marked members,
/// even if they are ignored.
/// </summary>
[Flags]
public enum IgnoreObsoleteMembersStrategy
{
/// <summary>
/// Maps <see cref="ObsoleteAttribute"/> marked members.
/// </summary>
None = 0,

/// <summary>
/// Will not map <see cref="ObsoleteAttribute"/> marked source or target members.
/// </summary>
Both = ~None,

/// <summary>
/// Ignores source <see cref="ObsoleteAttribute"/> marked members.
/// </summary>
Source = 1 << 0,

/// <summary>
/// Ignores target <see cref="ObsoleteAttribute"/> marked members.
/// </summary>
Target = 1 << 1,
}
6 changes: 6 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,10 @@ public sealed class MapperAttribute : Attribute
/// to keep track of and reuse existing target object instances.
/// </summary>
public bool UseReferenceHandling { get; set; }

/// <summary>
/// The ignore obsolete attribute strategy. Determines how <see cref="ObsoleteAttribute"/> marked members are mapped.
/// Defaults to <see cref="IgnoreObsoleteMembersStrategy.None"/>.
/// </summary>
public IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy { get; set; } = IgnoreObsoleteMembersStrategy.None;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Specifies options for obsolete ignoring strategy.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class MapperIgnoreObsoleteMembersAttribute : Attribute
{
/// <summary>
/// Specifies options for obsolete ignoring strategy.
/// </summary>
/// <param name="ignoreObsoleteStrategy">The strategy to be used to map <see cref="ObsoleteAttribute"/> marked members. Defaults to <see cref="IgnoreObsoleteMembersStrategy.Both"/>.</param>
public MapperIgnoreObsoleteMembersAttribute(IgnoreObsoleteMembersStrategy ignoreObsoleteStrategy = IgnoreObsoleteMembersStrategy.Both)
{
IgnoreObsoleteStrategy = ignoreObsoleteStrategy;
}

/// <summary>
/// The strategy used to map <see cref="ObsoleteAttribute"/> marked members.
/// </summary>
public IgnoreObsoleteMembersStrategy IgnoreObsoleteStrategy { get; }
}
10 changes: 10 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
Riok.Mapperly.Abstractions.EnumMappingStrategy
Riok.Mapperly.Abstractions.EnumMappingStrategy.ByName = 1 -> Riok.Mapperly.Abstractions.EnumMappingStrategy
Riok.Mapperly.Abstractions.EnumMappingStrategy.ByValue = 0 -> Riok.Mapperly.Abstractions.EnumMappingStrategy
Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy
Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy.Both = -1 -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy
Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy.None = 0 -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy
Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy.Source = 1 -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy
Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy.Target = 2 -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy
Riok.Mapperly.Abstractions.MapEnumAttribute
Riok.Mapperly.Abstractions.MapEnumAttribute.IgnoreCase.get -> bool
Riok.Mapperly.Abstractions.MapEnumAttribute.IgnoreCase.set -> void
Expand All @@ -19,6 +24,8 @@ Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingIgnoreCase.get -> bool
Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingIgnoreCase.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingStrategy.get -> Riok.Mapperly.Abstractions.EnumMappingStrategy
Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingStrategy.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.IgnoreObsoleteMembersStrategy.get -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy
Riok.Mapperly.Abstractions.MapperAttribute.IgnoreObsoleteMembersStrategy.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.MapperAttribute() -> void
Riok.Mapperly.Abstractions.MapperAttribute.PropertyNameMappingStrategy.get -> Riok.Mapperly.Abstractions.PropertyNameMappingStrategy
Riok.Mapperly.Abstractions.MapperAttribute.PropertyNameMappingStrategy.set -> void
Expand All @@ -33,6 +40,9 @@ Riok.Mapperly.Abstractions.MapperConstructorAttribute.MapperConstructorAttribute
Riok.Mapperly.Abstractions.MapperIgnoreAttribute
Riok.Mapperly.Abstractions.MapperIgnoreAttribute.MapperIgnoreAttribute(string! target) -> void
Riok.Mapperly.Abstractions.MapperIgnoreAttribute.Target.get -> string!
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.MapperIgnoreSourceAttribute
Riok.Mapperly.Abstractions.MapperIgnoreSourceAttribute.MapperIgnoreSourceAttribute(string! source) -> void
Riok.Mapperly.Abstractions.MapperIgnoreSourceAttribute.Source.get -> string!
Expand Down
13 changes: 11 additions & 2 deletions src/Riok.Mapperly/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ public MapperConfiguration(SymbolAccessor symbolAccessor, ISymbol mapperSymbol)
Array.Empty<IFieldSymbol>(),
Array.Empty<EnumValueMappingConfiguration>()
),
new PropertiesMappingConfiguration(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<PropertyMappingConfiguration>()),
new PropertiesMappingConfiguration(
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<PropertyMappingConfiguration>(),
Mapper.IgnoreObsoleteMembersStrategy
),
Array.Empty<DerivedTypeMappingConfiguration>()
);
}
Expand Down Expand Up @@ -66,7 +71,11 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho
.WhereNotNull()
.ToList();
var explicitMappings = _dataAccessor.Access<MapPropertyAttribute, PropertyMappingConfiguration>(method).ToList();
return new PropertiesMappingConfiguration(ignoredSourceProperties, ignoredTargetProperties, explicitMappings);
var ignoreObsolete = _dataAccessor.Access<MapperIgnoreObsoleteMembersAttribute>(method).FirstOrDefault() is not { } methodIgnore
? _defaultConfiguration.Properties.IgnoreObsoleteMembersStrategy
: methodIgnore.IgnoreObsoleteStrategy;

return new PropertiesMappingConfiguration(ignoredSourceProperties, ignoredTargetProperties, explicitMappings, ignoreObsolete);
}

private EnumMappingConfiguration BuildEnumConfig(MappingConfigurationReference configRef)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using Riok.Mapperly.Abstractions;

namespace Riok.Mapperly.Configuration;

public record PropertiesMappingConfiguration(
IReadOnlyCollection<string> IgnoredSources,
IReadOnlyCollection<string> IgnoredTargets,
IReadOnlyCollection<PropertyMappingConfiguration> ExplicitMappings
IReadOnlyCollection<PropertyMappingConfiguration> ExplicitMappings,
IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy
);
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Diagnostics;
Expand All @@ -21,11 +22,17 @@ protected MembersMappingBuilderContext(MappingBuilderContext builderContext, T m
{
BuilderContext = builderContext;
Mapping = mapping;
MemberConfigsByRootTargetName = GetMemberConfigurations();

_unmappedSourceMemberNames = GetSourceMemberNames();
TargetMembers = GetTargetMembers();

IgnoredSourceMemberNames = builderContext.Configuration.Properties.IgnoredSources;
IgnoredSourceMemberNames = builderContext.Configuration.Properties.IgnoredSources
.Concat(GetIgnoredObsoleteSourceMembers())
.ToHashSet();
var ignoredTargetMemberNames = builderContext.Configuration.Properties.IgnoredTargets
.Concat(GetIgnoredObsoleteTargetMembers())
.ToHashSet();

_ignoredUnmatchedSourceMemberNames = InitIgnoredUnmatchedProperties(IgnoredSourceMemberNames, _unmappedSourceMemberNames);
_ignoredUnmatchedTargetMemberNames = InitIgnoredUnmatchedProperties(
Expand All @@ -34,9 +41,13 @@ protected MembersMappingBuilderContext(MappingBuilderContext builderContext, T m
);

_unmappedSourceMemberNames.ExceptWith(IgnoredSourceMemberNames);
TargetMembers.RemoveRange(builderContext.Configuration.Properties.IgnoredTargets);

MemberConfigsByRootTargetName = GetMemberConfigurations();

// remove explicitly mapped ignored targets from ignoredTargetMemberNames
// then remove all ignored targets from TargetMembers, leaving unignored and explicitly mapped ignored members
ignoredTargetMemberNames.ExceptWith(MemberConfigsByRootTargetName.Keys);
TargetMembers.RemoveRange(ignoredTargetMemberNames);
}

public MappingBuilderContext BuilderContext { get; }
Expand Down Expand Up @@ -66,6 +77,32 @@ private HashSet<string> InitIgnoredUnmatchedProperties(IEnumerable<string> allPr
return unmatched;
}

private IEnumerable<string> GetIgnoredObsoleteTargetMembers()
{
var obsoleteStrategy = BuilderContext.Configuration.Properties.IgnoreObsoleteMembersStrategy;

if (!obsoleteStrategy.HasFlag(IgnoreObsoleteMembersStrategy.Target))
return Enumerable.Empty<string>();

return BuilderContext.SymbolAccessor
.GetAllAccessibleMappableMembers(Mapping.TargetType)
.Where(x => BuilderContext.SymbolAccessor.HasAttribute<ObsoleteAttribute>(x.MemberSymbol))
.Select(x => x.Name);
}

private IEnumerable<string> GetIgnoredObsoleteSourceMembers()
{
var obsoleteStrategy = BuilderContext.Configuration.Properties.IgnoreObsoleteMembersStrategy;

if (!obsoleteStrategy.HasFlag(IgnoreObsoleteMembersStrategy.Source))
return Enumerable.Empty<string>();

return BuilderContext.SymbolAccessor
.GetAllAccessibleMappableMembers(Mapping.SourceType)
.Where(x => BuilderContext.SymbolAccessor.HasAttribute<ObsoleteAttribute>(x.MemberSymbol))
.Select(x => x.Name);
}

private HashSet<string> GetSourceMemberNames()
{
return BuilderContext.SymbolAccessor.GetAllAccessibleMappableMembers(Mapping.SourceType).Select(x => x.Name).ToHashSet();
Expand Down
1 change: 1 addition & 0 deletions src/Riok.Mapperly/Symbols/FieldMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public FieldMember(IFieldSymbol fieldSymbol)

public string Name => _fieldSymbol.Name;
public ITypeSymbol Type => _fieldSymbol.Type;
public ISymbol MemberSymbol => _fieldSymbol;

Check warning on line 17 in src/Riok.Mapperly/Symbols/FieldMember.cs

View check run for this annotation

Codecov / codecov/patch

src/Riok.Mapperly/Symbols/FieldMember.cs#L17

Added line #L17 was not covered by tests
public bool IsNullable => _fieldSymbol.NullableAnnotation == NullableAnnotation.Annotated || Type.IsNullable();
public bool IsIndexer => false;
public bool CanGet => !_fieldSymbol.IsReadOnly && _fieldSymbol.IsAccessible();
Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Symbols/IMappableMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public interface IMappableMember

ITypeSymbol Type { get; }

ISymbol MemberSymbol { get; }

bool IsNullable { get; }

bool IsIndexer { get; }
Expand Down
1 change: 1 addition & 0 deletions src/Riok.Mapperly/Symbols/PropertyMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal PropertyMember(IPropertySymbol propertySymbol)

public string Name => _propertySymbol.Name;
public ITypeSymbol Type => _propertySymbol.Type;
public ISymbol MemberSymbol => _propertySymbol;
public bool IsNullable => _propertySymbol.NullableAnnotation == NullableAnnotation.Annotated || Type.IsNullable();
public bool IsIndexer => _propertySymbol.IsIndexer;
public bool CanGet => !_propertySymbol.IsWriteOnly && _propertySymbol.GetMethod?.IsAccessible() != false;
Expand Down
3 changes: 3 additions & 0 deletions test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ public TestObjectDto(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)

public int IgnoredIntValue { get; set; }

[Obsolete]
public int IgnoredObsoleteValue { get; set; }

public DateOnly DateTimeValueTargetDateOnly { get; set; }

public TimeOnly DateTimeValueTargetTimeOnly { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static partial class DeepCloningMapper
[MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))]
[MapperIgnoreSource(nameof(TestObject.IgnoredStringValue))]
[MapperIgnoreSource(nameof(TestObject.ImmutableHashSetValue))]
[MapperIgnoreObsoleteMembers]
public static partial TestObject Copy(TestObject src);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static partial class ProjectionMapper
[MapperIgnoreTarget(nameof(TestObjectDtoProjection.IgnoredIntValue))]
[MapperIgnoreSource(nameof(TestObjectProjection.IgnoredStringValue))]
[MapProperty(nameof(TestObjectProjection.RenamedStringValue), nameof(TestObjectDtoProjection.RenamedStringValue2))]
[MapperIgnoreObsoleteMembers]
private static partial TestObjectDtoProjection ProjectToDto(this TestObjectProjection testObject);

private static TestObjectDtoManuallyMappedProjection? MapManual(string str)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static partial class StaticTestMapper

[MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))]
[MapperIgnoreTarget(nameof(TestObjectDto.IgnoredStringValue))]
[MapperIgnoreObsoleteMembers]
public static partial TestObjectDto MapToDtoExt(this TestObject src);

public static TestObjectDto MapToDto(TestObject src)
Expand All @@ -53,6 +54,7 @@ public static TestObjectDto MapToDto(TestObject src)
)]
[MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))]
[MapperIgnoreTarget(nameof(TestObjectDto.IgnoredIntValue))]
[MapperIgnoreObsoleteMembers]
private static partial TestObjectDto MapToDtoInternal(TestObject testObject);

// disable obsolete warning, as the obsolete attribute should still be tested.
Expand Down
1 change: 1 addition & 0 deletions test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public TestObjectDto MapToDto(TestObject src)
nameof(TestObject.NullableUnflatteningIdValue),
nameof(TestObjectDto.NullableUnflattening) + "." + nameof(TestObjectDto.NullableUnflattening.IdValue)
)]
[MapperIgnoreObsoleteMembers]
private partial TestObjectDto MapToDtoInternal(TestObject testObject);

// disable obsolete warning, as the obsolete attribute should still be tested.
Expand Down
Loading

0 comments on commit 9ad8722

Please sign in to comment.