We do a lot of mapping where I work. We use and love AutoMapper.
We would like to validate our mapping profiles. We also often want to ignore some legacy fields. It would be great to use MemberList.Source for this. And it mostly works fine, unless we want some fields to have some sort of special treatment. AutoMapper gets upset during validation if we want to use extension methods on fields or even ValueResolvers.It claims said fields are not mapped.
Is this by design, a bug, am I “holding it wrong” or just missing something obvious? Error message and repro follows.
Both tests errors out with this message
AutoMapper.AutoMapperConfigurationException
# Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters
Source -> Destination (Source member list)
AutomapperRepro.Source -> AutomapperRepro.Destination (Source member list)
Unmapped properties:
FieldB
Code:
using AutoMapper;
using Xunit;
namespace AutomapperRepro;
public class MappingTests
{
[Fact]
public void MappingProfile_IsValid()
{
var config = new MapperConfiguration(config => config.AddProfile<MappingProfile>());
config.AssertConfigurationIsValid();
}
[Fact]
public void MappingProfileValueResolver_IsValid()
{
var config = new MapperConfiguration(config => config.AddProfile<MappingProfileValueResolver>());
config.AssertConfigurationIsValid();
}
}
public class MappingProfile: Profile
{
public MappingProfile()
{
CreateMap<Source, Destination>(MemberList.Source)
.ForMember(dest => dest.FieldBPadded, opt => opt.MapFrom(s => s.FieldB.PadFieldB()));
}
}
public class MappingProfileValueResolver : Profile
{
public MappingProfileValueResolver()
{
CreateMap<Source, Destination>(MemberList.Source)
.ForMember(dest => dest.FieldBPadded, opt => opt.MapFrom(new PaddingResolver(), src => src.FieldB));
}
}
public static class PaddingExtentions
{
public static string PadFieldB(this string src)
{
return src.PadLeft(10, '0');
}
}
public class PaddingResolver : IMemberValueResolver<object, object, string, string>
{
public string Resolve(object source, object destination, string sourceMember, string destinationMember,
ResolutionContext context)
{
return sourceMember.PadFieldB();
}
}
public record Source
{
public string FieldA { get; set; } = string.Empty;
public string FieldB { get; set; } = string.Empty;
}
public record Destination
{
public string FieldA { get; set; } = string.Empty;
public string FieldBPadded { get; set; } = string.Empty;
public string SomeLegacyFieldThatCanBeIgnored { get; set; } = string.Empty;
public string SomeLegacyFieldThatCanBeIgnored2 { get; set; } = string.Empty;
public string SomeLegacyFieldThatCanBeIgnored3 { get; set; } = string.Empty;
}
Both suggestions from #LucianBargaoanu are valid👍 Combining MapFrom with a transformer worked great for me:
public class MappingProfileValueTransformer : Profile
{
public MappingProfileValueTransformer()
{
CreateMap<Source, Destination>(MemberList.Source)
.ForMember(dest => dest.FieldBPadded, opt =>
{
opt.MapFrom(s => s.FieldB);
opt.AddTransform(dest => dest.PadFieldB());
});
}
}
Related
I'm new in AutoMapper and I have problem with mapping my generic class to generic dto (using AutoMapper). I have this class:
public class Document<T>
{
public string Id { get; set; }
public Tag[] Tags { get; set; }
public T? Data { get; set; }
}
and this dto:
public class DocumentDto<T>
{
public string Id { get; set; }
public string[] Tags { get; set; }
public T? Data { get; set; }
}
and I need to make two-way mapping.. I created mapper profile in which I define this mapping like this:
public MapperProfile()
{
CreateMap<Document<FinancialReport>, DocumentDto<FinancialReportDto>>()
.ForMember(docDto => docDto.Tags, opt => opt.MapFrom(doc => doc.Tags.Select(tag => tag.Value).ToArray()));
CreateMap<DocumentDto<FinancialReportDto>, Document<FinancialReport>>()
.ForMember(docDto => docDto.Tags, opt => opt.MapFrom(doc => doc.Tags.Select(tag => new Tag { Value = tag }).ToArray()));
}
And then I setting this profile in extension method for dependency injection for IMapper:
public static IServiceCollection AddAutoMapper(this IServiceCollection services)
{
services.AddSingleton<IMapper>(sp =>
{
var config = new MapperConfiguration(cfg => {
cfg.AddProfile<MapperProfile>();
});
return config.CreateMapper();
});
return services;
}
And after this all when I try remap from DocumentDto=>Document or vice versa I got error: AutoMapperMappingException: Missing type map configuration or unsupported mapping.
I tryed googled for hours but nothing helped... Any ideas what I'm doing wrong?
UPDATE 1:
I'm calling mapping like this:
public async Task<IResponse> UpdateFinancialReport(DocumentDto<FinancialReportDto> documentDto)
{
var doc = _mapper.Map<Document<FinancialReport>>(documentDto);
}
ok, I found the problem.. In MapperProfile I was missing mapping config for generic types:
CreateMap<FinancialReport, FinancialReportDto>()
.ReverseMap();
Unable to map byte[] or byte[]? (nullable byte[]) using AutoMapper ForCtorParam function, I am getting below error message
AutoMapper.AutoMapperConfigurationException : Unmapped members were
found. Review the types and members below. Add a custom mapping
expression, ignore, add a custom resolver, or modify the
source/destination type For no matching constructor, add a no-arg
ctor, add optional arguments, or map all of the constructor parameters
================================================================================== Model -> ModelDto (Destination member list)
AutoMapperForCtorParamTest+Model ->
AutoMapperForCtorParamTest+ModelDto (Destination member list) Unmapped
properties: Code LinkId
Sample code to recreate below
public class AutoMapperForCtorParamTest
{
private readonly IMapper mapper;
public AutoMapperForCtorParamTest()
{
mapper = new Mapper(new MapperConfiguration(
cfg =>
{
cfg.AddProfile<ModelMapperProfile>();
}));
}
[Fact]
public void MapAllProperties()
{
mapper.ConfigurationProvider.AssertConfigurationIsValid();
}
public class ModelMapperProfile : Profile
{
public ModelMapperProfile()
{
var defaultCode = Guid.NewGuid().ToByteArray();
CreateMap<Model, ModelDto>()
.ForCtorParam("type", cfg => cfg.MapFrom(x => 1))
.ForCtorParam("code", cfg => cfg.MapFrom<byte[]>(x => defaultCode))
.ForCtorParam("linkId", cfg => cfg.MapFrom<byte[]?>(x => null));
}
}
public class ModelDto
{
public ModelDto(int id, string name, int type, byte[] code, byte[]? linkId)
{
Id = id;
Name = name;
Type = type;
Code = code;
LinkId = linkId;
}
public int Id { get; }
public string Name { get; }
public int Type { get; }
public byte[] Code { get; }
public byte[]? LinkId { get; }
}
public class Model
{
public Model(int id, string name)
{
Id = id;
Name = name;
}
public int Id { get; }
public string Name { get; }
}
}
Note:
I am using dotnet core 3.1 with nullable types enabled
I don't want to use construct using or convert using functions
I am using Automapper v 10.0.0
Use:
.ForCtorParam("text", opt => opt.MapFrom<byte[]?>(src => null));
In my case (string) I fixed it with:
.ForCtorParam("text", opt => opt.MapFrom<string>(src => null));
I have the following code
IList<ConfigurationDto> result = new List<ConfigurationDto>();
foreach (var configuration in await configurations.ToListAsync())
{
var configurationDto = _mapper.Map<ConfigurationDto>(configuration);
configurationDto.FilePath = _fileStorage.GetShortTemporaryLink(configuration.FilePath);
result.Add(configurationDto);
}
return result;
How can I use automapper instead if foreach? I can map collection, but how to call _fileStorage.GetShortTemporaryLink for each item?
I have looked at AfterMap but I don't know how to get FilePath from dest and map it to src one by one. Can I use automapper for that?
public class ConfigurationDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Version { get; set; }
public DateTime CreateDateTime { get; set; }
public long Size { get; set; }
public string FilePath { get; set; }
}
You can use the IValueResolver interface to configure your map to map a property from a function. Something like the sample bellow.
public class CustomResolver : IValueResolver<Configuration, ConfigurationDto, string>
{
private readonly IFileStorage fileStorage;
public CustomResolver(IFileStorage fileStorage)
{
_fileStorage= fileStorage;
}
public int Resolve(Configuration source, ConfigurationDto destination, string member, ResolutionContext context)
{
return _fileStorage.GetShortTemporaryLink(source.FilePath);
}
}
Once we have our IValueResolver implementation, we’ll need to tell AutoMapper to use this custom value resolver when resolving a specific destination member. We have several options in telling AutoMapper a custom value resolver to use, including:
MapFrom<TValueResolver>
MapFrom(typeof(CustomValueResolver))
MapFrom(aValueResolverInstance)
Then you should configure your map to use the custom resolver for mapping the FilePath property on ConfigurationDto.
var configuration = new MapperConfiguration(cfg => cfg.CreateMap<Configuration, ConfigurationDto>()
.ForMember(dest => dest.FilePath, opt => opt.MapFrom<CustomResolver>()));
You can see more about custom value resolvers at this link: http://docs.automapper.org/en/stable/Custom-value-resolvers.html
I have two classes, a ViewModel and a Dto, that are basically identical except that the Dto has a field 'readonly long? Phone;' while the ViewModel has the a property 'string Phone { get; set; }'.
The only way I've found to get AutoMapper to work is by changing the ViewModel Property to a backing property:
public long? Phone { get; set; }
public string PhoneNumberString
{
get
{
var srv = DependencyResolver.Current.GetService<IPhoneNumberService>();
return srv.GetFormattedPhoneNumber(Phone);
}
set
{
var srv = DependencyResolver.Current.GetService<IPhoneNumberService>();
Phone = srv.GetLongPhoneNumber(value);
}
}
And then in AutoMapper, have a gigantic line to call the constructor:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<MyViewModel, MyDto>()
.ConstructUsing(src => new MyDto(
src.Phone
/* ...Some ~30 other parameters here... */))
.ReverseMap();
});
...There must be a better way to do this? I've tried these:
.ForSourceMember(x => x.PhoneNumberString, opt => opt.DoNotValidate())
and
.ForMember(x => x.PhoneNumberString, opt => opt.Ignore())
and
.ForMember(viewModel => viewModel.Phone, options => options.MapFrom<PhoneNumberResolver>());//PhoneNumberResolver implements IValueResolver<ProspectMaintenanceViewModel, ProspectMaintenanceDto, long?>
Which all give 'Core.DTO.MyDto needs to have a constructor with 0 args or only optional args.' when trying to map, and:
.ForMember(dest => dest.Phone, opt => opt.MapFrom(src => 5))
Which gives 'System.ArgumentException: 'Expression must be writeable' when trying to configure AutoMapper.
Is there some way I can make AutoMapper understand that it can entirely ignore PhoneNumberString (or, even better, some way by which I can make it map long? to string so I don't need the backing property) without having to use the dto's constructor?
Is there any special reason that requires your DTO to not have a default constructor?
I have all my fields as readonly so that I can include a constructor that modifies (e.g. 'Description = description?.Trim();') and validates (e.g. 'if (Phone.HasValue && Phone.ToString().Length != 10) throw ...') the parameters. This way I can ensure that the Dto, being a value object, is always in a valid state.
1) Mapping to readonly field
So you have a Dto class:
public class Dto
{
public readonly long? PhoneNumber;
}
And then you are trying to force AutoMapper to do this:
var dto = new Dto();
dto.PhoneNumber = 123; // <== ERROR! A readonly field cannot be assigned to.
AutoMapper cannot write to readonly fields or properties. In matter of fact you neither. Either turn your field into a property with protected or private setter:
public class Dto
{
public long? PhoneNumber { get; private set; }
}
or make it a regular field by removing the readonly keyword:
public class Dto
{
public long? PhoneNumber;
}
2) Custom value resolving
ASP.NET MVC
Use a ValueResolver:
public class StringPhoneNumberResolver : IValueResolver<Dto, ViewModel, string>
{
private readonly IPhoneNumberService _phoneNumberService;
public StringPhoneNumberResolver()
{
_phoneNumberService = DependencyResolver.Current.GetService<IPhoneNumberService>();
}
public string Resolve(Dto source, ViewModel destination, string destMember, ResolutionContext context)
{
return _phoneNumberService.GetFormattedPhoneNumber(source.PhoneNumber);
}
}
You should know that generally it is an anti-pattern to have service injection in a DTO or IValueResolver. AutoMapper should be dumb and all kind of injections and so on should be handled elsewhere. That being said, here is the AutoMapper configuration:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Dto, ViewModel>()
.ForMember(viewModel => viewModel.PhoneNumber, options =>
options.MapFrom<StringPhoneNumberResolver>());
});
If you want to reverse the process of long ==> string to string ==> long simply add another value resolver:
public class LongPhoneNumberResolver : IValueResolver<ViewModel, Dto, long?>
{
private readonly IPhoneNumberService _phoneNumberService;
public LongPhoneNumberResolver()
{
_phoneNumberService = DependencyResolver.Current.GetService<IPhoneNumberService>();
}
public long? Resolve(ViewModel source, Dto destination, long? destMember, ResolutionContext context)
{
return _phoneNumberService.GetLongPhoneNumber(source.PhoneNumber);
}
}
.NET Core
If you would operate in .NET Core environment, which fully supports IServiceCollection integration, you should add this AutoMapper configuration:
serviceCollection.AddAutoMapper(config =>
{
config.CreateMap<Dto, ViewModel>()
.ForMember(viewModel => viewModel.PhoneNumber, options =>
options.MapFrom<StringPhoneNumberResolver>());
}, typeof(Startup));
and then have IPhoneNumberServce automagically injected into value resolver:
public StringPhoneNumberResolver(IPhoneNumberService phoneNumberService)
{
_phoneNumberService = phoneNumberService;
}
For dependency injection I used automapper.extensions.microsoft.dependencyinjection package.
Well, I found the problem. It has absolutely nothing to do with what I thought it did. Mapping map long? to string works out of the box.
The problem I had was with an entirely different property.
I had the following structure:
public class MyDto
{
public readonly AddressDto BillingAddress;
public readonly AddressDto ShippingAddress;
public readonly long? Phone;
...
}
public class AddressDto
{
public readonly string Country;
public readonly string SubnationalEntity;
...
}
public class MyViewModel
{
public string BillingAddressCountry { get; set; }
public string BillingAddressSubnationalEntity { get; set; }
public string ShippingAddressCountry { get; set; }
public string ShippingAddressSubnationalEntity { get; set; }
public string Phone { get; set; }
...
}
It worked once I changed it to the following:
public class MyDto
{
public readonly AddressDto BillingAddress;
public readonly AddressDto ShippingAddress;
public readonly long? Phone;
...
}
public class AddressDto
{
public readonly string Country;
public readonly string SubnationalEntity;
...
}
public class MyViewModel
{
public string AddressViewModel BillingAddress { get; set; }
public string AddressViewModel ShippingAddress { get; set; }
public string Phone { get; set; }
...
}
public class AddressViewModel
{
public string Country { get; set; }
public string SubnationalEntity { get; set; }
...
}
This is probably a simple answer, but i'm just starting to get the hand of AutoMapper.
Anyway, i have a domain object like this:
public class User
{
public string Name { get; set; }
public FacebookUser FacebookUser { get; set; }
}
And a ViewModel like this:
public class UserViewModel
{
public string Name { get; set; }
public long FacebookUniqueId { get; set; }
}
Here's what i have in my AutoMapper configuration:
Mapper.CreateMap<User,UserViewModel>()
.ForMember(dest => dest.FacebookUniqueId, opt => opt.MapFrom(src => src.FacebookUser.FacebookUniqueId))
But it throws an exception when the FacebookUser object is null, which is to be expected.
How do i tell AutoMapper:
Map UserViewModel.FacebookUniqueId to User.FacebookUser.FacebookUniqueId, except for when it's null, then use 0.
Any ideas?
opt => opt.MapFrom(src => src.FacebookUser == null ? 0 : src.FacebookUser.FacebookUniqueId)
Yikes, if I had just checked if the first idea worked, thought the MapFrom method only took an expression that pointed to a property to resolve it that way...
The code you provided works on the latest Automapper. I investigated the sources a little and found the following code:
try
{
var result = _method((TSource)source.Value);
return source.New(result, MemberType);
}
catch (NullReferenceException)
{
return source.New(null, MemberType);
}
As you can see now Automapper catches all the Null exceptions while resolving chain mappings. The following test is green:
[TestFixture]
public class AutomapperChainingMappingTest
{
[Test]
public void ChainMapping_NullProperty_DefaultValueSet()
{
AutoMapper.Mapper.CreateMap<User, UserViewModel>()
.ForMember(x => x.FacebookUniqueId, o => o.MapFrom(x => x.FacebookUser.FacebookUniqueId));
var source = new User();
var model = AutoMapper.Mapper.Map<UserViewModel>(source);
Assert.AreEqual(model.FacebookUniqueId, default(long));
}
}
public class User
{
public FacebookUser FacebookUser { get; set; }
}
public class UserViewModel
{
public long FacebookUniqueId { get; set; }
}
public class FacebookUser
{
public long? FacebookUniqueId { get; set; }
}