admin管理员组文章数量:1292446
I'm experiencing an issue where AutoMapper appears to lose a custom comparer when mapping a HashSet property. I have a DTO that contains a collection of strings and a destination model that expects an IReadOnlySet. I want the resulting HashSet to use StringComparer.OrdinalIgnoreCase so that string comparisons are case insensitive.
However, when I use AutoMapper with a mapping configuration like this…
record ReadOnlySetTestModel(IReadOnlySet<string> Values);
record DtoWithCollection(IReadOnlyCollection<string> Values);
var dto = new DtoWithCollection(["a", "B"]);
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<DtoWithCollection, ReadOnlySetTestModel>()
.ForMember(dest => dest.Values, opt => opt.MapFrom(src =>
src.Values.Distinct(StringComparer.OrdinalIgnoreCase)
.ToHashSet(StringComparer.OrdinalIgnoreCase)
));
});
var mapper = config.CreateMapper();
var result = mapper.Map<ReadOnlySetTestModel>(dto);
Assert.Contains("A", result.Values); // Fails: "A" is not found even though "a" exists
the resulting HashSet loses its case-insensitive comparer.
Is there any way to configure AutoMapper to preserve the custom comparer for the HashSet during mapping? I've already tried using MapFrom and ConvertUsing, but it appears that AutoMapper reconstructs the collection internally, which loses the custom comparer. I have multiple nested mappings in production, so I cannot simply use a custom constructor mapping (e.g., ForCtorParam) for this one property.
When I create the HashSet manually using the following code, everything works as expected:
var hashSet = dto.Values
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var result = new ReadOnlySetTestModel(hashSet);
Assert.Contains("a", result.Values);
Assert.Contains("A", result.Values);
These test pass because the custom comparer is preserved.
Here is link to an easy to download UnitTest repro.
Environment:
- AutoMapper version: 13.0.1
- .NET version: 5.0+
I think it is an internal AutoMapper issue. But author closed it that I should take it here.
I'm experiencing an issue where AutoMapper appears to lose a custom comparer when mapping a HashSet property. I have a DTO that contains a collection of strings and a destination model that expects an IReadOnlySet. I want the resulting HashSet to use StringComparer.OrdinalIgnoreCase so that string comparisons are case insensitive.
However, when I use AutoMapper with a mapping configuration like this…
record ReadOnlySetTestModel(IReadOnlySet<string> Values);
record DtoWithCollection(IReadOnlyCollection<string> Values);
var dto = new DtoWithCollection(["a", "B"]);
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<DtoWithCollection, ReadOnlySetTestModel>()
.ForMember(dest => dest.Values, opt => opt.MapFrom(src =>
src.Values.Distinct(StringComparer.OrdinalIgnoreCase)
.ToHashSet(StringComparer.OrdinalIgnoreCase)
));
});
var mapper = config.CreateMapper();
var result = mapper.Map<ReadOnlySetTestModel>(dto);
Assert.Contains("A", result.Values); // Fails: "A" is not found even though "a" exists
the resulting HashSet loses its case-insensitive comparer.
Is there any way to configure AutoMapper to preserve the custom comparer for the HashSet during mapping? I've already tried using MapFrom and ConvertUsing, but it appears that AutoMapper reconstructs the collection internally, which loses the custom comparer. I have multiple nested mappings in production, so I cannot simply use a custom constructor mapping (e.g., ForCtorParam) for this one property.
When I create the HashSet manually using the following code, everything works as expected:
var hashSet = dto.Values
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var result = new ReadOnlySetTestModel(hashSet);
Assert.Contains("a", result.Values);
Assert.Contains("A", result.Values);
These test pass because the custom comparer is preserved.
Here is link to an easy to download UnitTest repro.
Environment:
- AutoMapper version: 13.0.1
- .NET version: 5.0+
I think it is an internal AutoMapper issue. But author closed it that I should take it here.
Share Improve this question asked Feb 13 at 8:19 Vojtěch VečeřaVojtěch Večeřa 391 silver badge1 bronze badge 2 |2 Answers
Reset to default 0The root cause is not that you are using a specific configured HashSet. It comes from the fact that you're using a record. When having a record you have to use .ConvertUsing()
and not .ForMember()
.
The reason why this doesn't fail or even creates a default hashset is because AutoMapper is collection aware and automatically maps properties with same names.
Here is some code that illustrates the right usage:
public static class Program
{
public static void Main()
{
var configuration = new MapperConfiguration(config =>
{
config.CreateMap<Source, DestinationRecord>()
.ConvertUsing(source => new DestinationRecord(new HashSet<string>(source.Values, StringComparer.OrdinalIgnoreCase)));
config.CreateMap<Source, DestinationClass>()
.ForMember(dest => dest.Values, opt => opt.MapFrom(src => new HashSet<string>(src.Values, StringComparer.OrdinalIgnoreCase)));
});
var mapper = configuration.CreateMapper();
var source = new Source(["a", "A"]);
var destinationRecord = mapper.Map<DestinationRecord>(source);
var destinationClass = mapper.Map<DestinationClass>(source);
foreach (var item in destinationRecord.Values)
{
Console.WriteLine(item);
}
foreach (var item in destinationClass.Values)
{
Console.WriteLine(item);
}
}
}
public record Source(IReadOnlyCollection<string> Values);
public record DestinationRecord(IReadOnlySet<string> Values);
public class DestinationClass
{
public required IReadOnlySet<string> Values { get; init; }
}
The only aspect that AutoMapper currently doesn't support and doesn't makes this more clear is the fact that this doesn't throw:
var configuration = new MapperConfiguration(config =>
{
config.CreateMap<Source, DestinationRecord>()
.ForMember(dest => dest.Values, config => config.MapFrom(source => source.Values))
});
// This should potentially throw, but currently doesn't.
configuration.AssertConfigurationIsValid();
At the moment ForMember
has no effect. What you observe is due to
- Both records having the same name
Values
for their property/constructor parameter - the automatic collection mapping (between
IReadOnlyCollection<string>
andIReadOnlySet<string>
) you get by default which registers a differentIEqualityComparer<string>
- default one.
You can demo this if you remove the default collection mappers:
var collMapper = cfg.Internal().Mappers
.Where(x => x.ToString().Contains("CollectionMapper"))
.FirstOrDefault();
cfg.Internal().Mappers.Remove(collMapper);
You should get an Exception
after that.
You can register a collection mapper on your own to have a different comparer:
cfg.CreateMap<IReadOnlyCollection<string>, IReadOnlySet<string>>()
.ConvertUsing(h =>
new HashSet<string>(h,
StringComparer.OrdinalIgnoreCase));
If you run your code now, it should "work" (even without removing the built-in ones).
However, this logic is supposed to be in ForMember
. We are still doing the mapping via the record constructors - relying on Values being the same name. And once we have non-default values there, the ForMember
doesn't run.
To force that you need to actually use ConstructUsing
:
cfg.CreateMap<DtoWithCollection, ReadOnlySetTestModel>()
.ConstructUsing(src => new ReadOnlySetTestModel(default))
.ForMember(dest => dest.Values, opt => opt.MapFrom(
src => src.Values.ToHashSet(StringComparer.OrdinalIgnoreCase)))
;
Unfortunately at this point the default collection mappers are still in use...this time between the result of ToHashSet
which is HashSet<string>
and IReadOnlySet<string>
.
So, the best solution with using ForMember
working is:
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<HashSet<string>, IReadOnlySet<string>>()
.ConvertUsing(h => h);
cfg.CreateMap<DtoWithCollection, ReadOnlySetTestModel>()
.ConstructUsing(src => new ReadOnlySetTestModel(default))
.ForMember(dest => dest.Values, opt => opt.MapFrom(
src => src.Values.ToHashSet(StringComparer.OrdinalIgnoreCase)))
;
});
This is a lot code to make it work, luckily as the answer by Oliver mentions - we can just fall back on using ConvertUsing
which turns off the automatic mappers, uses the constructor as we would have to anyway. The only possible "issue" is that it's not as clean with many properties:
cfg.CreateMap<DtoWithCollection, ReadOnlySetTestModel>()
.ConvertUsing(source => new ReadOnlySetTestModel(
source.Values
.ToHashSet(StringComparer.OrdinalIgnoreCase)));
本文标签: netC AutoMapper loses custom HashSet comparer when mapping to an initonly propertyStack Overflow
版权声明:本文标题:.net - C# AutoMapper loses custom HashSet comparer when mapping to an init-only property - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741556217a2385173.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
.ForMember
is not used by the mapper, like if you completely omit it (try commenting it out). I don't have experience with AutoMapper to tell you why. Check yourself (e.g. here is some issue with it). – Sinatr Commented Feb 13 at 10:10