How to map a child with automapper and convert using - c#

I am using automapper to map from model to dto. In my model I want to use a string where in my dto I use an Enum. While it is a nested child, I am using ForPath instead ForMember. To convert to string is easy, however to convert the string back to type I wrote a ValueConverter. Using a ValueConverter in combination with ForMember is working excellent, however now I need to use it with ForPath which is not possible. Are there any other solutions to solve this problem, while I cannot find it in the automapper documentation or on stack.
This is my MappingProfile
this part is working with member:
CreateMap<Dto, Model>()
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type.ToString()))
.ReverseMap()
.ForMember(dest => dest.Type, opt => opt.ConvertUsing(new StringToEnumConverter<Type>(), src => src.Type));
this part I need ForPath and ConvertUsing, this code is not allowed
CreateMap<Dto, Model>()
.ForPath(dest => dest.Type, opt => opt.MapFrom(src => src.Parent.Type.ToString()))
.ReverseMap()
.ForPath(dest => dest.Parent.Type, opt => opt.ConvertUsing(new StringToEnumConverter<Type>(), src => src.Type));
and this is my ValueConverter:
public class StringToEnumConverter<T> : IValueConverter<string, T> where T : struct
{
public T Convert(string source, ResolutionContext context)
{
if (!string.IsNullOrEmpty(source))
{
if (Enum.TryParse(source, out T result))
{
return result;
}
}
return default;
}
}

Thanks to Lucian Bargaoanu I came up with this solution. I rewrote the valueConverter to be just a static class and use MapFrom to convert.
public static class StringEnumExtension
{
public static TaskType ToTaskType(this string source)
{
return ToEnum<TaskType>(source);
}
public static TaskQuestionType ToTaskQuestionType(this string source)
{
return ToEnum<TaskQuestionType>(source);
}
private static T ToEnum<T>(string source) where T : struct
{
if (!string.IsNullOrEmpty(source))
{
if (Enum.TryParse(source, out T result))
{
return result;
}
}
return default;
}
}
And I updated the mapping like this:
CreateMap<TaskDto, TaskModel>()
.ForPath(dest => dest.TaskType, opt => opt.MapFrom(src => src.TaskDefinition.TaskType.ToString()))
.ReverseMap()
.ForPath(dest => dest.TaskDefinition.TaskType, opt => opt.MapFrom(src => src.TaskType.ToTaskType()));

I prefer additional maps within the same profile over using ForPath. This way I can still use my custom value resolvers:
public class DstObject
{
public int AnotherProperty { get; set; }
public DstChildObject DstChildObject { get; set; }
}
public class DstChildObject
{
public string SomeProperty { get; set; }
}
public class MyMappingProfile : Profile
{
public MyMappingProfile()
{
this.CreateMap<SourceType, DstObject>()
.ForMember(dst => dst.AnotherProperty, opt => opt.MapFrom(src => src.AnotherProperty))
.ForMember(dst => dst.DstChildObject, opt => opt.MapFrom(src => src))
;
this.CreateMap<SourceType, DstChildObject>()
.ForMember(dst => dst.SomeProperty, opt => opt.MapFrom(src => src.SomeProperty))
;
}
}

Related

Automapping to extend a class

There are two classes:
public class User
{
public int id;
public string username;
// lots more...
}
public class UserEx : User
{
public string extraData;
}
And this is the CreateMap that is currently being used:
profile.CreateMap< Tuple<User, string>, UserEx>()
.ForMember( d => d.id, opt => opt.MapFrom( s => s.Item1.id ) )
.ForMember( d => d.username, opt => opt.MapFrom( s => s.Item1.username ) )
// lots and lots more mappings of s.Item1 to d
.ForMember( d => d.extraData, opt => opt.MapFrom( s => s.Item2.extraData ) );
The User class has a ton of fields, is there some automatic way to get it to map s.Item1 to the destination?
I did look at the doc's on flattening > IncludeMembers and came up with the following which does NOT work:
.Ignore( d => d.extraData );
profile.CreateMap< Tuple<User, string>, UserEx>()
.IncludeMembers( t => t.Item1 )
.ForMember( d => d.extraData, opt => opt.MapFrom( s => s.Item2.extraData ) );
I took #PanagiotisKanavos's and kept it simple:
profile.CreateMap< User, UserEx>()
.Ignore( d => d.extraData );
UserEx MakeUserEx(User u, string extraData)
{
var userEx = mapper.Map<UserEx>(u);
userEx.extraData = extraData;
return userEx;
}
var listOfUserEx = from u in UserList
join ex in extraDataList on u.id equals ex.id
select MakeUserEx(u, ex.extraData)
I tried to figure out how to make the MakeUser a lambda function, but this works well and after a bit I figured it was time to move on.

AutoMapper - Map from object to enum

I have this class:
public class EntityClass
{
public bool IsExempt { get; set; }
public bool IsOverdue { get; set; }
public SourceEnum Status { get; set; }
}
And these two enums:
public enum SourceEnum
{
NotSet = 0,
InProgress,
Submitted,
AssessmentComplete,
Complete
}
[Flags]
public enum DestinationEnum
{
None = 0,
[Description("Exempt")]
Exempt = 1,
[Description("Unset")]
Unset = 2,
[Description("Overdue")]
Overdue = 3,
[Description("In Progress")]
InProgress = 4,
[Description("Submitted")]
Submitted = 5,
[Description("Conf. Pending")]
ConfirmationPending = 6,
[Description("Complete")]
Completed = 7
}
I want to map from EntityClass to DestinationEnum, I tried it this way:
var config = new MapperConfiguration(cfg => cfg.CreateMap<EntityClass, DestinationEnum>()
.ForMember(dest => dest, opt => opt.MapFrom(source => source.IsExempt ? DestinationEnum.Exempt : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.IsOverdue ? DestinationEnum.Overdue : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.Status == SourceEnum.InProgress ? DestinationEnum.InProgress : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.Status == SourceEnum.Complete ? DestinationEnum.Completed : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.Status == SourceEnum.AssessmentComplete ? DestinationEnum.ConfirmationPending : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.Status == SourceEnum.Submitted ? DestinationEnum.Submitted : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.Status == SourceEnum.NotSet ? DestinationEnum.Unset : DestinationEnum.None)));
var mapper = config.CreateMapper();
var entityClassObj = new EntityClass { IsExempt = true, IsOverdue = true, Status = SourceEnum.InProgress };
var result = mapper.Map<DestinationEnum>(entityClassObj);
When I run my code, it returns:
Custom configuration for members is only supported for top-level
individual members on a type.
I tried the solution from here but unfortunately the IncludeMembers method is not available. I am using AutoMapper 6.0.2.
As #Lucian mentioned, you need to implement the Custom Type Converter for your scenario.
Solution 1
public class EntityClassToDestinationEnumConverter : ITypeConverter<EntityClass, DestinationEnum>
{
public DestinationEnum Convert(EntityClass src, DestinationEnum dest, ResolutionContext context)
{
if (src.IsExempt)
return DestinationEnum.Exempt;
if (src.IsOverdue)
return DestinationEnum.Overdue;
switch (src.Status)
{
case SourceEnum.InProgress:
return DestinationEnum.InProgress;
case SourceEnum.Complete:
return DestinationEnum.Completed;
case SourceEnum.AssessmentComplete:
return DestinationEnum.ConfirmationPending;
case SourceEnum.Submitted:
return DestinationEnum.Submitted;
case SourceEnum.NotSet:
return DestinationEnum.Unset;
}
return DestinationEnum.None;
}
}
Your Mapping Profile or MapperConfiguration to map from EntityClass to DestinationEnum.
MapperConfiguration _config = new MapperConfiguration(config =>
{
config.CreateMap<EntityClass, DestinationEnum>()
.ConvertUsing<EntityClassToDestinationEnumConverter>();
});
Solution 2
Or writing a method for the logic without a custom type resolver class.
public static class Helpers
{
public static DestinationEnum ConvertFromEntityClassToDestinationEnum(EntityClass src)
{
if (src.IsExempt)
return DestinationEnum.Exempt;
if (src.IsOverdue)
return DestinationEnum.Overdue;
switch (src.Status)
{
case SourceEnum.InProgress:
return DestinationEnum.InProgress;
case SourceEnum.Complete:
return DestinationEnum.Completed;
case SourceEnum.AssessmentComplete:
return DestinationEnum.ConfirmationPending;
case SourceEnum.Submitted:
return DestinationEnum.Submitted;
case SourceEnum.NotSet:
return DestinationEnum.Unset;
}
return DestinationEnum.None;
}
}
MapperConfiguration _config = new MapperConfiguration(config =>
{
config.CreateMap<EntityClass, DestinationEnum>()
.ConvertUsing(src => Helpers.ConvertFromEntityClassToDestinationEnum(src));
});
Demo Solution 1 & 2 # .NET Fiddle

AutoMapper core Condition not working

I want the null value during mapping DTO to DBO model to be ignored. This is the code:
DTO / DBO models have both property named items:
public virtual ICollection<price_list_item> items { get; set; }
DBO constructor:
public price_list()
{
this.items = new List<price_list_item>();
}
DTO constructor has no propert initialization
public price_list()
{
}
AutoMapper Profile:
this.CreateMap<DTO.price_list, DBO.price_list>()
.ForMember(m => m.id, src => src.Ignore())
.ForMember(m => m.currency_id, src => src.MapFrom(f => f.currency))
.ForMember(dest => dest.items, opt => opt.Condition(src => (src.items != null)))
API Controller:
[HttpPut]
[Route("{id:long}")]
public async Task<DTO.price_list> UpdateOneAsync(long id, [FromBody]DTO.price_list payload)
{
if (payload == null)
{
throw new ArgumentNullException("payload");
}
Console.WriteLine(payload.items == null);
var _entity = await this.IDataRepository.price_lists
.Where(w => w.id == id)
.Include(i => i.items)
.FirstOrDefaultAsync();
if (_entity == null)
{
NotFound();
return null;
}
Console.WriteLine(_entity.items.Count);
// map fields to existing model
this.IMapper.Map<DTO.price_list, DBO.price_list>(payload, _entity);
Console.WriteLine(_entity.items.Count);
When I send to API a JSON without any sign of 'items' property, Console output is:
True
1200 // price list in dbo has 1200 items
0 // here I need to have still 1200 items
What am I doing wrong? Why the condition is not respected and items property is not 'skiped' ?
Thanks
Lucian thanks, PreCondition solved the problem. This is working code:
this.CreateMap<DTO.price_list, DBO.price_list>()
.ForMember(m => m.id, src => src.Ignore())
.ForMember(m => m.currency_id, src => src.MapFrom(f => f.currency))
.ForMember(dest => dest.items, opt => opt.PreCondition(src => (src.items != null)))

Map viewmodel to entity from EF using Automapper while ignore null values

I'm trying to map a viewmodel to an entity from EF. I have gotten so far that I can map all properties from the viewmodel to the entity but I am trying to ignore all properties that is null (the data in the viewmodel comes from a form and I don't want to put null as the pk for example). I want to be able to do something like this:
IUserDetails objUserDetails = GetDataFromForm();
var user = db.Users.FirstOrDefault();
user.UpdateUser(objUserDetails);
This is what I have come up with so far:
public static class UserExtensions
{
public static void UpdateUser(this IUser user, IUserDetails userDetails)
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<IUserDetails, IUser>()
.UseDestinationValue()
.IgnoreNullValues()
.ForMember(dest => dest.AddressLine1, opt => opt.MapFrom(src => src.Address1))
.ForMember(dest => dest.AddressLine2, opt => opt.MapFrom(src => src.Address2))
.ForMember(dest => dest.LName, opt => opt.MapFrom(src => src.LastName))
.ForMember(dest => dest.UserGUID, opt => opt.MapFrom(src => src.GUID));
});
var mapper = config.CreateMapper();
mapper.Map<IUserDetails, IUser>(userDetails, user);
}
private static IMappingExpression<TSource, TDest> UseDestinationValue<TSource, TDest>(this IMappingExpression<TSource, TDest> expression)
{
expression.ForAllMembers(opt => opt.UseDestinationValue());
return expression;
}
private static IMappingExpression<TSource, TDest> IgnoreNullValues<TSource, TDest>(this IMappingExpression<TSource, TDest> expression)
{
expression.ForAllMembers(opt => opt.Condition((src, dest, srcVal, destVal, c) => { Debugger.Break(); return srcVal != null; }));
return expression;
}
}
As you can see I have put a breakpoint in IgnoreNullValues() and when I debug the code I can see that srcVal is never anything else than null, however all the other values looks good. What am I missing?
Update:
Apparently this is due to a bug in 5.1.1, downgrading to 5.0.2 made it all work.

"An invalid or incomplete configuration was used while creating a SessionFactoryā€¯ NHibernate

I am getting this error while creating SessionFactory. Here is the code, if some can check this out.
class NHibernateHelper {
private static ISessionFactory _sessionFactory;
private static ISessionFactory SessionFactory {
get {
if (_sessionFactory == null) {
InitializeSessionFactory();
}
return _sessionFactory;
}
}
private static void InitializeSessionFactory() {
_sessionFactory = Fluently.Configure().
Database(MsSqlConfiguration.MsSql2008.ConnectionString
("Server=tcp:z4m56fgh.database.windows.net,1433;Database=db;User ID=user;Password=xxxxx;Trusted_Connection=False;Encrypt=True;Connection Timeout=30;").
ShowSql()).
Mappings(m => m.FluentMappings.AddFromAssemblyOf<House>()).
ExposeConfiguration(cfg => new SchemaExport(cfg).Create(false, true)).
BuildSessionFactory();
}
public static ISession OpenSession() {
return SessionFactory.OpenSession();
}
}
Mappings:
public class HouseMap : ClassMap<House> {
public HouseMap() {
Id(x => x.Id);
References(x => x.Owner).Cascade.All();
References(x => x.Rooms).Cascade.All();
References(x => x.Consumptions).Cascade.All();
}
}
public class ConsumptionMap : ClassMap<Consumption> {
public ConsumptionMap() {
Id(x => x.Id);
Map(x => x.Type);
Map(x => x.AvgDay);
Map(x => x.AvgMonth);
Map(x => x.AvgYear);
}
}
public class RoomMap : ClassMap<Room> {
public RoomMap() {
Id(x => x.Id);
Map(x => x.Name);
Map(x => x.Number);
Component<TemperatureData>(x => x.TemperatureData,
t => {
t.Map(x => x.TemperatureCurrent, "Current");
t.Map(x => x.TemperatureSet, "Set");
});
Component<RoomFeatures>(x => x.Features,
f => {
f.Map(x => x.Shutters, "ShuttersUp");
f.Map(x => x.Lights, "LightsOn");
});
}
}
This exact class NHibernateHelper works with another Model, so it must be something wrong with my mappings. Also, it still does not work if i exclude everything from HouseMap, i.e. comment on the References.
It is being a little difficult to realize what is the source of your problem without the classes, although, a guess would be at the Consumptions property. If it is a list (as it seems by its name) it should be mapped with HasMany instead of References.
Besides, maybe you could attach the stack trace with the InnerException. This could give us a clue.

Categories