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
Related
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))
;
}
}
Hello,
I'm currently building an ASP.NET backend.
For the mapping of the entities and the model object I'm using AutoMapper(Version 4.2.1).
When I send a post request with Postman for creating a task, I always get an exception which says:
Missing type map configuration or unsupported mapping.
Mapping types: NewTask -> Task TaskManager.Web.Api.Models.NewTask ->
TaskManager.Data.Entities.Task
Destination path: Task
Source value: TaskManager.Web.Api.Models.NewTask
I tried several things out but never came to an result which worked.
Here is my AutoMapperAdapter.cs:
I always get the errormessage in this file
namespace TaskManager.Common.TypeMapping {
public class AutoMapperAdapter : IAutoMapper {
public T Map<T>(object objectToMap) {
try {
return Mapper.Map<T>(objectToMap);
} catch (Exception ex) {
throw ex;
}
}
}
}
And the AddTaskMaintenance Processor:
namespace TaskManager.Web.Api.MaintenanceProcessing {
public class AddTaskMaintenanceProcessor : IAddTaskMaintenanceProcessor {
private readonly IAutoMapper _autoMapper;
private readonly IAddTaskQueryProcessor _queryProcessor;
public AddTaskMaintenanceProcessor(IAddTaskQueryProcessor queryProcessor, IAutoMapper autoMapper) {
_queryProcessor = queryProcessor;
_autoMapper = autoMapper;
}
public Models.Task AddTask(NewTask newTask) {
var taskEntity = _autoMapper.Map<Data.Entities.Task>(newTask);
_queryProcessor.AddTask(taskEntity);
var task = _autoMapper.Map<Models.Task>(taskEntity);
return task;
}
}
}
The mapping of the model objects to the entities and reverse looks like that:
namespace TaskManager.Web.Api.AutoMappingConfiguration {
public class NewTaskToTaskEntityAutoMapperTypeConfigurator : IAutoMapperTypeConfigurator {
public void Configure() {
Mapper.Initialize(conf => {
conf.CreateMap<NewTask, TaskManager.Data.Entities.Task>()
.ForMember(opt => opt.Version, x => x.Ignore())
.ForMember(opt => opt.CreatedBy, x => x.Ignore())
.ForMember(opt => opt.TaskId, x => x.Ignore())
.ForMember(opt => opt.CreatedDate, x => x.Ignore())
.ForMember(opt => opt.CompletedDate, x => x.Ignore())
.ForMember(opt => opt.Status, x => x.Ignore())
.ForMember(opt => opt.Users, x => x.Ignore());
});
}
}
}
And the other file:
namespace TaskManager.Web.Api.AutoMappingConfiguration {
public class TaskEntitiyToTaskAutoMapperTypeConfigurator : IAutoMapperTypeConfigurator {
public void Configure() {
Mapper.Initialize(conf => {
conf.CreateMap<Task, Models.Task>()
.ForMember(opt => opt.Links, x => x.Ignore())
.ForMember(opt => opt.Status, x => x.Ignore())
.ForMember(opt => opt.Assignees, x => x.ResolveUsing<TaskAssigneesResolver>());
});
}
}
}
My questions:
What could probably be the problem with my code?
Do I have to upgrade the version of the AutoMapper?
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)))
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.
I have an entity that contains a DbGeography type that has a multi-polygon. I have mapped the DbGeography to a string[][]:
internal class DbGeographyAutoMapper : Profile
{
public DbGeographyAutoMapper()
{
CreateMap<DbGeography, string[][]>()
.ConvertUsing(geo =>
{
var maxElement = geo.ElementCount + 1;
var rings = new string[geo.ElementCount.Value][];
for (var elementIndex = 1; elementIndex < maxElement; elementIndex++)
{
var currentElement = geo.ElementAt(elementIndex);
var latLngs = new string[currentElement.PointCount.Value];
var max = currentElement.PointCount + 1;
for (var i = 1; i < max; i++)
{
var point = currentElement.PointAt(i).EndPoint;
latLngs[i - 1] = $"{Math.Round(point.Latitude.Value, 4)},{Math.Round(point.Longitude.Value, 4)}";
}
rings[elementIndex - 1] = latLngs;
}
return rings;
});
}
}
And the entity to a model that contains a string[][]:
internal class WireCenterAutoMapper : Profile
{
public WireCenterAutoMapper()
{
CreateMap<WireCenter, WirecenterModel>()
.ForMember(m => m.Boundary, m => m.MapFrom(wc => wc.Boundary)) // This is the DbGeography to string[][]
.ForMember(m => m.CLLI, m => m.MapFrom(wc => wc.CLLI))
.ForMember(m => m.Id, m => m.MapFrom(wc => wc.Id))
.ForMember(m => m.Name, m => m.MapFrom(wc => wc.OCN))
.ForMember(m => m.Owner, m => m.MapFrom(wc => wc.Incumbent))
.ForMember(m => m.State, m => m.MapFrom(wc => wc.State));
}
}
When I try and project it to an IQueryable ...
[EnableQuery(PageSize = 500)]
public async Task<IQueryable<WirecenterModel>> Get([FromUri] BoundingBox intersects = null)
{
return (await _wireCentersRepository.Find(intersects)).ProjectTo<WirecenterModel>();
}
... it blows up because the DbGeography uses a convert using. I would like to have that property mapped when converting it from the entity to the model, but ignored when projecting it to an EF Query. I did mark it as NotMapped...
/// <summary>
/// The Wire Center's boundary
/// </summary>
[NotMapped]
public string[][] Boundary { get; set; }
... but this did not help. How do I do this?
I had a similar issue where I didn't want to map specifically during a ProjectTo().
What worked for myself was the ExplicitExpansion setting:
internal class WireCenterAutoMapper : Profile
{
public WireCenterAutoMapper()
{
CreateMap<WireCenter, WirecenterModel>()
.ForMember(m => m.Boundary, opt => opt.ExplicitExpansion())
//...Followed by the rest of the code
}
}
With this, LINQ projections won't map this value, leaving it null, unless you explicitly request it to be mapped in a projection.