Automapper, map by naming convention - c#

I'm using automapper to map my entities. But entities have different structure.
Source:
public class SourceEntity
{
public string Name { get; set; }
public Type Type { get; set; }
public Communication SelectedCommunication { get; set; }
}
public enum Type
{
Type1=1,
Typ2
}
[Flags]
public enum Communication
{
Phone =1,
Email =2,
Post =4
}
Also I have HasFlag() extension method that will return true if flag is selected.
Destination entity:
public class DestinationEntity
{
public string Name { get; set; }
public bool Type1_PhoneSelected { get; set; }
public bool Type1_EmailSelected { get; set; }
public bool Type1_PostSelected { get; set; }
public bool Type2_PhoneSelected { get; set; }
public bool Type2_EmailSelected { get; set; }
public bool Type2_PostSelected { get; set; }
}
My map:
CreateMap<SourceEntity, DestinationEntity>()
.ForMember(v => v.Name, opt => opt.MapFrom(i => i.Name));
But I can't figure out the best way to map Types properties.
Is it possible to map it without typing something like:
.ForMemeber(v=>v.Test1_PhoneSelected, opt=>opt.MapFrom(i=>i.SelectedCommunication.HasFlag(Communication.Phone)))
.ForMemeber(v=>v.Test2_PhoneSelected, opt=>opt.MapFrom(i=>i.SelectedCommunication.HasFlag(Communication.Phone)))
For each of this properties.
Is there any way to map by naming convention?
Or any other ways?

You can use custom value resolvers
Although AutoMapper covers quite a few destination member mapping
scenarios, there are the 1 to 5% of destination values that need a
little help in resolving. Many times, this custom value resolution
logic is domain logic that can go straight on our domain. However, if
this logic pertains only to the mapping operation, it would clutter
our source types with unnecessary behavior. In these cases,
AutoMapper allows for configuring custom value resolvers for
destination members.
Example of custom value resolver:
public class YourCustomResolver
: IMemberValueResolver<object, object, Communication, bool>
{
private Communication _communication;
public YourCustomResolver(
Communication communication)
{
}
public bool Resolve(
object source,
object destination,
Communication sourceMember,
bool destMember,
ResolutionContext context)
{
return _communication == sourceMember;
}
}
Your mapping will look like this:
CreateMap<SourceEntity, DestinationEntity>()
.ForMember(dest => dest.Type1_PhoneSelected, opt => opt.ResolveUsing(new YourCustomResolver(Communication.Phone), src => src.SelectedCommunication))
.ForMember(dest => dest.Type1_EmailSelected, opt => opt.ResolveUsing(new YourCustomResolver(Communication.Email), src => src.SelectedCommunication))
.ForMember(dest => dest.Type1_PostSelected , opt => opt.ResolveUsing(new YourCustomResolver(Communication.Post) , src => src.SelectedCommunication))
.ForMember(dest => dest.Type2_PhoneSelected, opt => opt.ResolveUsing(new YourCustomResolver(Communication.Phone), src => src.SelectedCommunication))
.ForMember(dest => dest.Type2_EmailSelected, opt => opt.ResolveUsing(new YourCustomResolver(Communication.Email), src => src.SelectedCommunication))
.ForMember(dest => dest.Type2_PostSelected , opt => opt.ResolveUsing(new YourCustomResolver(Communication.Post) , src => src.SelectedCommunication));

Related

How can I include a child object mapping using AutoMapper when the source objects have no inheritance relationship?

I have an object model something like this:
public class Concert {
public Band Band { get; set; }
public ConcertVenue Venue { get; set; }
}
public class TicketOrder {
public Concert Concert { get; set; }
public string CustomerName { get; set; }
}
// DTOs for email and web views:
public class ConcertDto {
public string Artist { get; set; }
public string Venue { get; set; }
}
public class TicketOrderDto : ConcertDto {
public string CustomerName { get; set; }
}
I'm using AutoMapper to map domain objects to DTOs. The DTOs here have an inheritance relationship that doesn't exist in the domain model (because when I send an email about a ticket order, I want to include all the information about the concert)
I have a mapping defined like this:
config.CreateMap<Concert, ConcertDto>()
.ForMember(dto => dto.Artist, opt => opt.MapFrom(concert => concert.Band.Name))
.ForMember(dto => dto.Venue, opt => opt.MapFrom(concert => concert.GetVenueSummary());
config.CreateMap<TicketOrder, ConcertDto>()
.ForMember(dto => dto.Artist, opt => opt.MapFrom(concert => concert.Band.Name))
.ForMember(dto => dto.Venue, opt => opt.MapFrom(concert => concert.GetVenueSummary())
.ForMember(dto => dto.CustomerName, optn.MapFrom(order => order.Customer.FullName))
;
There's some duplication in those maps, and what I want to do is to reuse the Concert > ConcertViewData mapping when I map the TicketOrderDto:
cfg.CreateMap<TicketOrder, TicketOrderDto>()
// This is what I *want* but isn't valid AutoMapper syntax:
.IncludeMembers(dto => dto, order => order.Concert)
.ForMember(dto => dto.CustomerName, optn.MapFrom(order => order.Customer.FullName));
but this fails with:
System.ArgumentOutOfRangeException: Only member accesses are allowed.
dto => dto (Parameter 'memberExpressions')
at AutoMapper.ExpressionExtensions.EnsureMemberPath(LambdaExpression exp, String name)
Calling .IncludeBase<> doesn't work, because ConcertOrder doesn't derive from Concert.
Is there an easy way to import one map into another but specify that it should map from a child object of the source type? i.e. "hey, please map source.Child onto this, and then run the regular source > this mapping?"
I am going to make an assumption here, but I believe the mapping should be from TicketOrder to TicketOrderDto, and not ConcertDto (which contains no CustomerName property) as the given models don't match the given mapping configuration.
In that case, you should be able to use .AfterMap() on ticket mapper configuration to map from Concert to ConcertDto.
cfg.CreateMap<TicketOrder, TicketOrderDto>()
.ForMember(d => d.CustomerName, o => o.MapFrom(s => s.Order.Customer.Name))
.AfterMap((s, d, context) => context.Mapper.Map(s.Concert, d));

AutoMapper condition Mapping Issue

I have got a problem using Automapper when conditionally Mapping a table.
Here is an example:
public class DepositsVm : IMapFrom<Deposits>
{
public long DepId { get; set; }
public AddressDto Address { get; set; }
public void Mapping(Profile profile)
{
profile.CreateMap<Deposits, DepositsVm>()
.ForMember(d => d.DepId, s => s.MapFrom(s => s.DepId))
.ForMember(d => d.Address, opt => opt.MapFrom(s => s.ProcessingId != null ? s.DataProcessing.GridCollect.Grid.Address : s.Reduction.DataCollect.Tower.Address));
}
}
This results in an Object reference error.
I can see we can use https://docs.automapper.org/en/stable/Conditional-mapping.html#preconditions but this allow to check for only one condition. I expect to map a table using different join condition in failure and success scenarios.
But this works, because I'm projecting each Address separately. But, this is not desired. Because both are from Same Address Table
public class DepositsVm : IMapFrom<Deposits>
{
public long DepId { get; set; }
public AddressDto GridAddress { get; set; }
public AddressDto TowerAddress { get; set; }
public void Mapping(Profile profile)
{
profile.CreateMap<Deposits, DepositsVm>()
.ForMember(d => d.DepId, s => s.MapFrom(s => s.DepId))
.ForMember(d => d.GridAddress, opt => opt.MapFrom(s => s.DataProcessing.GridCollect.Grid.Address));
.ForMember(d => d.TowerAddress, opt => opt.MapFrom(s => s.Reduction.DataCollect.Tower.Address));
}
}
Either some part of s.DataProcessing.GridCollect.Grid.Address is null, or some part of s.Reduction.DataCollect.Tower.Address is null

AutoMapper mapping between enum and its integer values fails with ReverseMap

The application is built with DDD approach, with a separate set of persistence models. I called database object, or dbo:
public class ParentDbo
{
public int ParentId { get; set; }
public int TypeId { get; set; }
}
public class ChildDbo
{
public int ChildId { get; set; }
public ParentDbo Parent { get; set; }
public int RetryNumber { get; set; }
}
We have a simple model to look at: a parent and a child relationship. The RetryNumber presents the enum value in the database.
On retrieving data, it uses Dapper to first query the database, and use its splitOn feature to map data into them. This part is irrelevant but I will show it anyway for completeness:
const string sql = "SELECT * FROM XXX ....";
using (var cnt = _dbConnectionFactory.CreateConnection())
{
var childDbos = await cnt.QueryAsync<ChildDbo, ParentDbo, ChildDbo>(
sql: sql,
map: (childDbo, parentDbo) =>
{
childDbo.Parent = parentDbo;
return childDbo;
},
splitOn: "ParentId"
);
}
Dapper has limitation that it couldn't map data to private complex objects. That's mainly the reason why I have to have 2 sets of models. I would like to encapsulate the data and logic within domain models, with private setters and other techniques.
Here are my domain models:
public class Parent
{
public int Id { get; private set; }
public int TypeId { get; private set; }
public Parent(int parentId, int typeId)
{
// Validations
this.Id = parentId;
this.TypeId = typeId;
}
}
public class Child
{
public int Id { get; private set; }
public Parent Parent { get; private set; }
public Attempt Attempt { get; private set; }
public Child(int childId, Parent parent, Attempt attempt)
{
// Validations
this.Id = childId;
this.Parent = parent;
this.Attempt = attempt;
}
}
For domain models, I don't want public setters, and parameter-less constructors.
The Attempt is the enum with integer backing values:
public enum Attempt
{
Original = 1,
FirstRetry = 2,
SecondRetry = 3,
LastRetry = 4
}
Lastly, I want to use AutoMapper to map between Dbos and the domain models. Here is the mapping:
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Child, ChildDbo>()
.ForMember(dest => dest.ChildId, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.RetryNumber, opts => opts.MapFrom(src => (int)src.Attempt))
.ReverseMap();
CreateMap<Parent, ParentDbo>()
.ForMember(dest => dest.ParentId, opts => opts.MapFrom(src => src.Id))
.ReverseMap();
}
}
I want to have two-ways mappings so I use ReverseMap().
.Net Fiddle demo: https://dotnetfiddle.net/saEHWd
It maps domain models to dbos without problem:
But its reverse, mapping from dbos to domain models, is throwing exceptions:
Unhandled exception. System.ArgumentException: Program+Child needs to have a constructor with 0 args or only optional args. (Parameter 'type')
at lambda_method18(Closure , Object , Child , ResolutionContext )
at AutoMapper.Mapper.MapCore[TSource,TDestination](TSource source, TDestination destination, ResolutionContext context, Type sourceType, Type destinationType, IMemberMap memberMap)
at AutoMapper.Mapper.Map[TSource,TDestination](TSource source, TDestination destination)
at AutoMapper.Mapper.Map[TDestination](Object source)
at Program.Main()
I've tried to remove the enum property and everything worked so I'm pretty sure it's the enum mapping that's having issues.
As far as I can see in your fiddle you are trying to map from ChildDbo to Parent and there is no mapping setup for it. Change the mapping code to:
var child2 = mapper.Map<Child>(childDbo);
And since there is mismatch in third Child's ctor param and source property names change map to:
CreateMap<Child, ChildDbo>()
.ForMember(dest => dest.ChildId, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.RetryNumber, opts => opts.MapFrom(src => (int)src.Attempt))
.ReverseMap()
.ConstructUsing((dbo, ctx) => new Child(dbo.ChildId, ctx.Mapper.Map<Parent>(dbo.Parent), (Attempt)dbo.RetryNumber));
See here
Or rename third Child's ctor parameter to retryNumber:
public Child(int childId, Parent parent, Attempt retryNumber)
see here.
or use ForCtorParam:
CreateMap<Child, ChildDbo>()
.ForMember(dest => dest.ChildId, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.RetryNumber, opts => opts.MapFrom(src => (int)src.Attempt))
.ReverseMap()
.ForCtorParam("attempt", opt => opt.MapFrom(dbo => dbo.RetryNumber))
Here.

AutoMapper mapping from multiple properties to complex objects fails with ReverseMap and custom value resolver

I have issues on reverse mapping multiple properties back to complex objects, even with custom value resolvers.
Here are the persistence model:
public class EmailDbo
{
public int EmailId { get; set; }
public DateTime DateCreated { get; set; }
public DateTime? DateSent { get; set; }
public string SendTo { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public bool DownloadAvailable { get; set; }
public DateTime? AdminDateSent { get; set; }
public string AdminEmail { get; set; }
public string AdminSubject { get; set; }
public string AdminBody { get; set; }
public int StatusId { get; set; }
}
I have Dapper map data from database and fill in this model.
Here are the domain models I want to map back and forth with the persistence model:
public class Email
{
public string SendTo { get; private set; }
public string Subject { get; private set; }
public string Body { get; private set; }
public DateTime? DateSent { get; private set; }
public Email(string sendTo, string subject, string body, DateTime? dateSent = null)
{
// Validations
this.SendTo = sendTo;
this.Subject = subject;
this.Body = body;
this.DateSent = dateSent;
}
}
public enum EmailTaskStatus
{
Sent = 1,
Unsent = 2
}
public class EmailTask
{
public int Id { get; private set; }
public DateTime DateCreated { get; private set; }
public Email PayerEmail { get; private set; }
public Email AdminEmail { get; private set; }
public bool DownloadAvailableForAdmin { get; private set; }
public EmailTaskStatus Status { get; private set; }
public EmailTask(int emailTaskId, DateTime dateCreated, Email payerEmail, Email adminEmail,
bool downloadAvailable, EmailTaskStatus status)
{
// Validations
this.Id = emailTaskId;
this.DateCreated = dateCreated;
this.PayerEmail = payerEmail;
this.AdminEmail = adminEmail;
this.DownloadAvailableForAdmin = downloadAvailable;
this.Status = status;
}
}
I would like to use a value object called Email for both the payer and admin email. You can tell they're just stored flatten in the database/persistence model. And the payer email is required but not the admin email.
I have the mapping configured like following:
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<EmailTask, EmailDbo>()
.ForMember(dest => dest.EmailId, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.SendTo, opts => opts.MapFrom(src => src.PayerEmail.SendTo))
.ForMember(dest => dest.Subject, opts => opts.MapFrom(src => src.PayerEmail.Subject))
.ForMember(dest => dest.Body, opts => opts.MapFrom(src => src.PayerEmail.Body))
.ForMember(dest => dest.DateSent, opts => opts.MapFrom(src => src.PayerEmail.DateSent))
.ForMember(dest => dest.DownloadAvailable, opts => opts.MapFrom(src => src.DownloadAvailableForAdmin))
.ForMember(dest => dest.AdminEmail, opts =>
{
opts.PreCondition(src => (src.AdminEmail != null));
opts.MapFrom(src => src.AdminEmail.SendTo);
})
.ForMember(dest => dest.AdminSubject, opts =>
{
opts.PreCondition(src => (src.AdminEmail != null));
opts.MapFrom(src => src.AdminEmail.Subject);
})
.ForMember(dest => dest.AdminBody, opts =>
{
opts.PreCondition(src => (src.AdminEmail != null));
opts.MapFrom(src => src.AdminEmail.Body);
})
.ForMember(dest => dest.AdminDateSent, opts =>
{
opts.PreCondition(src => (src.AdminEmail != null));
opts.MapFrom(src => src.AdminEmail.DateSent);
})
.ForMember(dest => dest.StatusId, opts => opts.MapFrom(src => (int)src.Status))
.ReverseMap()
.ForCtorParam("status", opts => opts.MapFrom(src => src.StatusId))
.ForMember(dest => dest.PayerEmail, opts => opts.MapFrom<PayerEmailValueResolver>())
.ForMember(dest => dest.AdminEmail, opts => opts.MapFrom<AdminEmailValueResolver>());
}
}
After ReverseMap(), I want to grab multiple properties and construct the complex object Email. Hence I define two custom value resolvers for that:
public class PayerEmailValueResolver : IValueResolver<EmailDbo, EmailTask, Email>
{
public Email Resolve(EmailDbo emailDbo, EmailTask emailTask, Email email, ResolutionContext context)
{
return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);
}
}
public class AdminEmailValueResolver : IValueResolver<EmailDbo, EmailTask, Email>
{
public Email Resolve(EmailDbo emailDbo, EmailTask emailTask, Email email, ResolutionContext context)
{
if (String.IsNullOrWhiteSpace(emailDbo.AdminEmail) &&
String.IsNullOrWhiteSpace(emailDbo.AdminSubject) &&
String.IsNullOrWhiteSpace(emailDbo.AdminBody) &&
!emailDbo.AdminDateSent.HasValue)
{
return null;
}
return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);
}
}
As always, the mapping from the domain model to the Dbo works fine:
but not the other way, from Dbo to domain model. It's throwing exceptions:
Unhandled exception. System.ArgumentException: Program+EmailTask needs to have a constructor with 0 args or only optional args. (Parameter 'type')
at lambda_method32(Closure , Object , EmailTask , ResolutionContext )
at AutoMapper.Mapper.MapCore[TSource,TDestination](TSource source, TDestination destination, ResolutionContext context, Type sourceType, Type destinationType, IMemberMap memberMap)
at AutoMapper.Mapper.Map[TSource,TDestination](TSource source, TDestination destination)
at AutoMapper.Mapper.Map[TDestination](Object source)
.Net Fiddle demo: https://dotnetfiddle.net/DcTsPG
I wonder if AutoMapper confuses about those two Email objects: payer email and admin email, because they're both are Email type.
In reverse map AutoMapper is failing to create an instance of EmailTask.
Add a parameterless constructor to your EmailTask class -
public EmailTask()
{
// AutoMapper use only
}
Also, since your value resolvers are creating instance of Email, add a parameterless constructor to your Email class too -
public Email()
{
// AutoMapper use only
}
Finally, modify the PayerEmail and AdminEmail properties in EmailTask class so they can be set publicly -
public Email PayerEmail { get; set; }
public Email AdminEmail { get; set; }
That should solve your issue.
EDIT :
#David Liang, after reading your comment I'd say, to suit your scenario in light of DDD, you might need to modify your current mapping approach.
The thing is, when you are mapping EmailDbo from EmailTask, the process is easier because EmailDbo is a DTO type class with no parameterized constructor. Therefore, the property mapping only is enough to do the job.
But when you are trying to map EmailTask from EmailDbo, you are trying to instantiate a domain model class which has very strictly defined parameterized constructor that takes complex types as parameters, and is trying to protect how it's properties can and cannot be accessed from outside. Therefore, the .ReverseMap() approach you are using currently will not be very helpful, because the property mapping only will not be enough to provide you all the constructor parameters needed to instantiate the class. There's also AutoMapper's naming convention in the play.
Following is a mapping configuration for EmailTask from EmailDbo, where the reverse mapping is separated out and the value resolvers are refactored out into a helper class. The forward mapping remained unchanged.
CreateMap<EmailDbo, EmailTask>()
.ConstructUsing((s, d) =>
new EmailTask(
s.EmailId,
s.DateCreated,
Helper.GetPayerEmail(s),
Helper.GetAdminEmail(s),
s.DownloadAvailable,
(EmailTaskStatus)s.StatusId))
.IgnoreAllPropertiesWithAnInaccessibleSetter();
The Helper class -
public class Helper
{
public static Email GetPayerEmail(EmailDbo emailDbo)
{
return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);
}
public static Email GetAdminEmail(EmailDbo emailDbo)
{
if (string.IsNullOrWhiteSpace(emailDbo.AdminEmail) && string.IsNullOrWhiteSpace(emailDbo.AdminSubject)
&& string.IsNullOrWhiteSpace(emailDbo.AdminBody) && !emailDbo.AdminDateSent.HasValue)
{
return null;
}
return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);
}
}
Here is the complete fiddle - https://dotnetfiddle.net/2MxSdt

Default mapping configuration for inherited classes in AutoMapper

Is it possible to create a default destination mapping in AutoMapper ?
Source classes:
class SourceA {
public string X { get; set; }
}
class SourceB {
public string Y { get; set; }
}
Destination classes:
class DestBase {
public List<string> Z { get; set; }
}
class DestA : DestBase {
public string X { get; set; }
}
class DestB : DestBase {
public string Y { get; set; }
}
And the mapping configuration contains the following:
cfg.CreateMap<SourceA, DestA>()
.ForMember(dest => dest.Z, src => src.MapFrom(s => null));
cfg.CreateMap<SourceB, DestB>()
.ForMember(dest => dest.Z, src => src.MapFrom(s => null));
Is it possible to create a default mapping for all destination classes inheriting the DestBase to avoid the repeated .ForMember(...) lines ?
eg. something like:
cfg.CreateMap<object, DestBase>
.ForMember(dest => dest.Z, src => src.MapFrom(s => new List<string>()));
In principle yes, with the Include method, but there is a caveat.
If you define a map from source type object, this map would match all types. Maybe you can introduce an interface ISource for the source types that should be affected by this mapping.
So it could look like this:
class SourceA : ISource {
public string X { get; set; }
}
class SourceB : ISource {
public string Y { get; set; }
}
cfg.CreateMap<ISource, DestBase>
.Include<SourceA, DestA>
.Include<SourceB, DestB>
.Include<SourceC, DestC>
.ForMember(dest => dest.Z, , o => o.MapFrom(src => new List<string>()));
cfg.CreateMap<SourceA, DestA>()
.ForMember(dest => dest.X, o => o.MapFrom(src => src.X));
cfg.CreateMap<SourceB, DestB>()
.ForMember(dest => dest.Y, o => o.MapFrom(src => src.Y));
// still need to create a map even if no additional properties are to be mapped
cfg.CreateMap<SourceC, DestC>();
Note that you still need to create maps for all included types, even if there are no additional properties to map.

Categories