Hello Guys I'm Having a Lot of trouble mapping some models using AutoMapper and I wanted to know if you could point me in the right direction.
I have some Entities as follow;
public class Camp
{
public int CampId { get; set; }
public string Name { get; set; }
public string Moniker { get; set; }
public Location Location { get; set; }
public DateTime EventDate { get; set; } = DateTime.MinValue;
public int Length { get; set; } = 1;
public ICollection<Talk> Talks { get; set; }
}
public class Talk
{
public int TalkId { get; set; }
public Camp Camp { get; set; }
public string Title { get; set; }
public string Abstract { get; set; }
public int Level { get; set; }
public Speaker Speaker { get; set; }
}
And the corresponding DTO's
public class CampModel
{
public string Name { get; set; }
public string Moniker { get; set; }
public DateTime EventDate { get; set; } = DateTime.MinValue;
public int Length { get; set; } = 1;
public string Venue { get; set; }
public string LocationAddress1 { get; set; }
public string LocationAddress2 { get; set; }
public string LocationAddress3 { get; set; }
public string LocationCityTown { get; set; }
public string LocationStateProvince { get; set; }
public string LocationPostalCode { get; set; }
public string LocationCountry { get; set; }
public ICollection<TalkModel> Talks { get; set; }
}
public class TalkModel
{
public int TalkId { get; set; }
public string Title { get; set; }
public string Abstract { get; set; }
public int Level { get; set; }
}
I wanted To use automapper on my controller as follow:
[Route("api/[controller]")]
public class CampsController : ControllerBase
{
private readonly ICampRepository _repository;
private readonly IMapper _mapper;
public CampsController(ICampRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
[HttpGet]
public async Task<ActionResult<CampModel[]>> Get(bool includeTalks = false)
{
try
{
var camps = await _repository.GetAllCampsAsync(includeTalks);
var mapper = _mapper.Map<CampModel[]>(camps);
return mapper;
}
catch (Exception e)
{
return StatusCode(StatusCodes.Status500InternalServerError, "Database failure" + " Message: " + e);
}
}
}
I'm returning the camps on my repository like this:
public async Task<Camp[]> GetAllCampsByEventDate(DateTime dateTime, bool includeTalks = false)
{
_logger.LogInformation($"Getting all Camps");
IQueryable<Camp> query = _context.Camps
.Include(c => c.Location);
if (includeTalks)
{
query = query
.Include(c => c.Talks)
.ThenInclude(t => t.Speaker);
}
// Order It
query = query.OrderByDescending(c => c.EventDate)
.Where(c => c.EventDate.Date == dateTime.Date);
return await query.ToArrayAsync();
}
I already registered my automapper on Startup.Cs
services.AddAutoMapper(typeof(CampProfile).Assembly);
Using the profile like this:
public class CampProfile : Profile
{
public CampProfile()
{
this.CreateMap<Camp, CampModel>()
.ForMember(c => c.Venue, o => o.MapFrom(m => m.Location.VenueName))
.ForMember(c => c.Talks, o => o.MapFrom(m => m.Talks))
.ReverseMap();
}
}
But when i try to hit my endpoint i get the following error:
Message: AutoMapper.AutoMapperMappingException: Error mapping types.
Mapping types:
Object -> CampModel[]
System.Object -> CoreCodeCamp.Models.CampModel[]
---> AutoMapper.AutoMapperMappingException: Error mapping types.
Mapping types:
Camp -> CampModel
CoreCodeCamp.Data.Camp -> CoreCodeCamp.Models.CampModel
Type Map configuration:
Camp -> CampModel
CoreCodeCamp.Data.Camp -> CoreCodeCamp.Models.CampModel
Destination Member:
Talks
---> AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping.
Mapping types:
Talk -> TalkModel
CoreCodeCamp.Data.Talk -> CoreCodeCamp.Models.TalkModel
What am I doing wrong? I think the problem is related to the public ICollection<Talk> Talks { get; set; } property. Thanks in advance
Just add mapper between Talk and TalkModel like below:
public class CampProfile : Profile
{
public CampProfile()
{
this.CreateMap<Talk, TalkModel>();
this.CreateMap<Camp, CampModel>()
.ForMember(c => c.Venue, o => o.MapFrom(m => m.Location.VenueName))
//.ForMember(c => c.Talks, o => o.MapFrom(m => m.Talks))
.ReverseMap();
}
}
From the code you gave above, you need to config the map between Talk and TalkModel, check out Nested Mappings in AutoMapper.
Related
For context, I'm in the process of migrating our EF6 Db Context to EF Core 3. Why EF Core 3 only? Currently we're not able to upgrade to the latest EF Core version because of project constraints. We're still using .NET Framework 4.5.6, we're slowly upgrading.
Libaries used
EF Core 3.1.19
Devart.Data.MySql.Entity.EFCore 8.19
The models
public class AutomatedInvestigation
{
public int AutomatedSearchScreenshotId { get; set; }
public int OrderId { get; set; }
public int OrderLineItemId { get; set; }
public int ServiceId { get; set; }
public int? ComponentId { get; set; }
public OrderLineItemResults Result { get; set; }
public int? PageSourceDocumentId { get; set; }
public string Errors { get; set; } = string.Empty;
public DateTime CreateDateTime { get; set; }
public DateTime EditDateTime { get; set; }
public SearchRequestParameters SearchParameters { get; set; }
public Service Service { get; set; }
public Subject Subject { get; set; }
public Order Order { get; set; }
public OrderLineItem OrderLineItem { get; set; }
public virtual Component Component { get; set; }
}
[ComplexType]
public class SearchRequestParameters
{
public SearchRequestParameters()
{
this.Serialized = string.Empty;
}
[NotMapped]
[JsonIgnore]
public string Serialized
{
get { return JsonConvert.SerializeObject(Parameters); }
set
{
if (string.IsNullOrEmpty(value)) return;
var parameters = JsonConvert.DeserializeObject<SearchParameters>(value);
Parameters = parameters ?? new SearchParameters();
}
}
public SearchParameters Parameters { get; set; }
}
[ComplexType]
public class SearchParameters
{
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public DateTime DOB { get; set; }
public string State { get; set; } = string.Empty;
}
The model builder (excluded irrelevant code)
internal static ModelBuilder BuildAutomationInvestigationModel(this ModelBuilder modelBuilder)
{
var entityTypeBuilder = modelBuilder.Entity<AutomatedInvestigation>();
entityTypeBuilder.OwnsOne(s => s.SearchParameters, sa =>
{
sa.OwnsOne(p => p.Parameters, pa =>
{
pa.Property(p => p.FirstName);
pa.Property(p => p.LastName);
pa.Property(p => p.DOB);
pa.Property(p => p.State);
});
});
entityTypeBuilder.ToTable("automated_investigations")
.HasKey(p => p.AutomatedSearchScreenshotId);
entityTypeBuilder.MapProperties()
.MapRelations();
return modelBuilder;
}
private static EntityTypeBuilder<AutomatedInvestigation> MapProperties(this EntityTypeBuilder<AutomatedInvestigation> entityTypeBuilder)
{
entityTypeBuilder.Property(p => p.AutomatedSearchScreenshotId).HasColumnName("automated_investigation_id");
entityTypeBuilder.Property(p => p.OrderId).HasColumnName("order_id").IsRequired();
entityTypeBuilder.Property(p => p.OrderLineItemId).HasColumnName("order_line_item_id").IsRequired();
entityTypeBuilder.Property(p => p.ServiceId).HasColumnName("service_id").IsRequired();
entityTypeBuilder.Property(p => p.ComponentId).HasColumnName("component_id").IsRequired(false);
entityTypeBuilder.Property(p => p.Result).IsRequired();
entityTypeBuilder.Property(p => p.PageSourceDocumentId).IsRequired(false);
entityTypeBuilder.Property(e => e.Errors)
.IsRequired()
.HasColumnName("errors")
.HasColumnType("mediumtext");
entityTypeBuilder.Property(p => p.CreateDateTime).HasColumnName("create_datetime").IsRequired();
entityTypeBuilder.Property(p => p.EditDateTime).HasColumnName("edit_datetime").IsRequired();
return entityTypeBuilder;
}
The error
I've tried adding HasColumnName but throws the same error. I've also tried using [Owned] annotation instead of the OwnsOne on the model builder but throws the same error. Also tried just specifying "SearchParameters" but will throw unknown column on "Parameters".
I'm trying to map two different objects to objects that are derived from an interface. Additionally, I need to have another property mapped to the derived types from the dtos. Given this object structure:
public interface ICoverage
{
string Name { get; set; }
string Code { get; set; }
}
public class CoverageA : ICoverage
{
public string Name { get; set; }
public string Code { get; set; }
public string Current { get; set; }
}
public class CoverageB : ICoverage
{
public string Name { get; set; }
public string Code { get; set; }
public bool HasRecord { get; set; }
}
public class Application
{
public int ApplicationId { get; set; }
public string Code { get; set; }
public List<ICoverage> Coverages { get; set; }
public Application()
{
Coverages = new List<ICoverage>();
}
}
public class StagingDto
{
public string Referrer { get; set; }
public string Code { get; set; }
public CoverageADto CoverageA { get; set; }
public CoverageBDto CoverageB { get; set; }
}
public class CoverageADto
{
public string Current { get; set; }
}
public class CoverageBDto
{
public bool HasRecord { get; set; }
}
This mapping below works but I am wondering if there is a better way to do it:
cfg.CreateMap<StagingDto, Application>()
.AfterMap((src, dest) => dest.Coverages.Add(new CoverageB()
{
HasRecord = src.CoverageB.HasRecord,
Code = src.Code
}))
.AfterMap((src, dest) => dest.Coverages.Add(new CoverageA()
{
Current = src.CoverageA.Current,
Code = src.Code
}));
Ideally I'd like to stay away from having to create any extension method.
For me it looks a bit better:
cfg.CreateMap<StagingDto, Application>()
.ForMember(dest => dest.Coverages,
opt => opt.ResolveUsing(src => new ICoverage[]
{
new CoverageA
{
Current = src.CoverageA.Current,
Code = src.Code
},
new CoverageB
{
HasRecord = src.CoverageB.HasRecord,
Code = src.Code
}
}));
We have a pretty common scenario where we use Automapper to map DTOs and Entities. As you would expect a lot of properties are 1=1 in both classes with some exceptions here and there.
As number of classes and properties has grown, sometimes developers forget to keep properties in sync when renaming or removing them.
Can you suggest how we could reliably detect "unmapped" properties, preferably automatically?
For such a problem I would recommend to use the GetUnmappedPropertyNames method of IMapper itself. Code and Test should explain the idea below:
The condition
(z.PropertyType.IsValueType || z.PropertyType.IsArray || z.PropertyType == typeof(string))
will detect unmapped properties from Value Types like int, enum, Guid, DateTime, all Nullable value types bool?, Decimal?, Guid?, and string.
And such filter let your test to ignore mapping for Entity Navigation properties kind of:
public virtual Class NavigationProperty {get;set}
public virtual IList<Class> CollectionNavigationProperty { get; set; }
Code and test:
[Test]
public void Mapping_Profile_Must_Not_Have_Unmapped_Properties()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<TestProfile>();
});
var mapper = config.CreateMapper();
var unmappedProperties = GetUnmappedSimpleProperties(mapper);
Assert.AreEqual(unmappedProperties.Count, 0);
}
private List<UnmappedProperty> GetUnmappedSimpleProperties(IMapper mapper)
{
return mapper.ConfigurationProvider.GetAllTypeMaps()
.SelectMany(m => m.GetUnmappedPropertyNames()
.Where(x =>
{
var z = m.DestinationType.GetProperty(x);
return z != null && (z.PropertyType.IsValueType || z.PropertyType.IsArray || z.PropertyType == typeof(string));
})
.Select(n => new UnmappedProperty
{
DestinationTypeName = m.DestinationType.Name,
PropertyName = n,
SourceTypeName = m.SourceType.Name
})).ToList();
}
internal class UnmappedProperty
{
public string PropertyName { get; set; }
public string DestinationTypeName { get; set; }
public string SourceTypeName { get; set; }
public override string ToString()
{
return $"{this.PropertyName}: {this.SourceTypeName}->{this.DestinationTypeName}";
}
}
Proving test at your service:
[Test]
public void Test_Mapping_Profile_Must_Detect_Unmapped_Properties()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<TestMappingProfile>();
});
ar mapper = config.CreateMapper();
var unmappedProperties = GetUnmappedSimpleProperties();
Assert.AreEqual(unmappedProperties.Count, 12);
}
public class TestMappingProfile : Profile
{
public TestMappingProfile()
{
CreateMap<Source, DestinationValid>();
CreateMap<Source, DestinationInvalid>();
}
}
internal class Source
{
public string Test1 { get; set; }
public int Test2 { get; set; }
public int? Test3 { get; set; }
public decimal Test4 { get; set; }
public string[] Test5 { get; set; }
public Guid Test6 { get; set; }
public Guid? Test7 { get; set; }
public TransactionRealm Test8 { get; set; }
public bool? Test9 { get; set; }
public bool Test10 { get; set; }
public DateTime Test11 { get; set; }
public DateTime? Test12 { get; set; }
}
internal class DestinationValid
{
public string Test1 { get; set; }
public int Test2 { get; set; }
public int? Test3 { get; set; }
public decimal Test4 { get; set; }
public string[] Test5 { get; set; }
public Guid Test6 { get; set; }
public Guid? Test7 { get; set; }
public TransactionRealm Test8 { get; set; }
public bool? Test9 { get; set; }
public bool Test10 { get; set; }
public DateTime Test11 { get; set; }
public DateTime? Test12 { get; set; }
}
internal class DestinationInvalid
{
public string Test1X { get; set; }
public int Test2X { get; set; }
public int? Test3X { get; set; }
public decimal Test4X { get; set; }
public string[] Test5X { get; set; }
public Guid Test6X { get; set; }
public Guid? Test7X { get; set; }
public TransactionRealm Test8X { get; set; }
public bool? Test9X { get; set; }
public bool Test10X { get; set; }
public DateTime Test11X { get; set; }
public DateTime? Test12X { get; set; }
}
where TransactionRealm is an example of enum:
public enum TransactionRealm
{
Undefined = 0,
Transaction = 1,
Fee = 2,
}
There is also alternative approach with MapperConfiguration.AssertConfigurationIsValid() method, which can be using either in unit tests and in run time. AssertConfigurationIsValid() method throws exception with detailed description of all detected unmapped properties. In business logic i'd recommend (for better performance) to initialize mapper in service static constructor with custom MapperFactory helper:
public class MyBLL
{
private static IMapper _mapper;
static MyBLL()
{
_mapper = MapperFactory.CreateMapper<DtoToEntityDefaultProfile>();
}
}
public static class MapperFactory
{
public static IMapper CreateMapper<T>() where T : Profile, new()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<T>();
});
/// AssertConfigurationIsValid will detect
/// all unmapped properties including f.e Navigation properties, Nested DTO classes etc.
config.AssertConfigurationIsValid();
config.CompileMappings();
return config.CreateMapper();
}
}
I have the following classes
public class Group
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<GroupTier> Tiers { get; set; }
}
public class GroupTier : IEntity
{
public int Id { get; set; }
public int GroupId { get; set; }
public int Tier { get; set; }
public decimal Amount { get; set; }
public virtual Group Group { get; set; }
}
I am trying to map to the following ViewModel
public class GroupViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<decimal> Tiers { get; set; }
}
using the configuration
configuration.CreateMap<Group, GroupViewModel>()
.ForMember(m => m.Tiers, opt => opt.MapFrom(u => u.Tiers.OrderBy(q => q.Tier).Select(q => q.Amount)));
I am using EF6 to query from the database. I am having trouble when the Group.Tiers is null. How can I handle the null value?
When I use the this configuration
configuration.CreateMap<Group, GroupViewModel>()
.ForMember(m => m.Tiers, opt => opt.MapFrom(u => u.Tiers == null ? new List<decimal>() : u.Tiers.OrderBy(q => q.Tier).Select(q => q.Amount)));
I am getting this error
Cannot compare elements of type 'System.Collections.Generic.ICollection'
I'm having a little difficulty mapping a domain model to a view model, using AutoMapper.
My controller code is:
//
// GET: /Objective/Analyst
public ActionResult Analyst(int id)
{
var ovm = new ObjectiveVM();
ovm.DatePeriod = new DateTime(2013, 8,1);
var objectives = db.Objectives.Include(o => o.Analyst).Where(x => x.AnalystId == id).ToList();
ovm.ObList = Mapper.Map<IList<Objective>, IList<ObjectiveVM>>(objectives);
return View(ovm);
}
I am getting an error on the ovm.ObList = Mapper.... (ObList is underlined in red with the error):
'ObList': cannot reference a type through an expression; try 'Objectives.ViewModels.ObjectiveVM.ObList' instead
My Objective Class is:
public class Objective
{
public int ObjectiveId { get; set; }
public int AnalystId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public Analyst Analyst { get; set; }
}
My ObjectiveVM (view model) is:
public class ObjectiveVM
{
public DateTime DatePeriod { get; set; }
public class ObList
{
public int ObjectiveId { get; set; }
public int AnalystId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string AnalystName { get; set; }
public bool Include { get; set; }
}
}
In my startup/global.asax.cs I have used AutoMapper to map the Objective to the ObjectiveVM:
Mapper.CreateMap<Objective, ObjectiveVM.ObList>()
.ForMember(dest => dest.Include, opt => opt.Ignore())
.ForMember(dest => dest.AnalystName, opt => opt.MapFrom(y => (y.Analyst.AnalystName)));
Any help would be much appreciated,
Mark
Ok, thanks for all the suggestions - what I've ended up with is:
Controller:
//
// GET: /Objective/Analyst
public ActionResult Analyst(int id)
{
var ovm = new ObjectiveVM().obList;
var objectives = db.Objectives.Include(o => o.Analyst).Where(x => x.AnalystId == id).ToList();
ovm = Mapper.Map<IList<Objective>, IList<ObjectiveVM.ObList>>(objectives);
var ovm2 = new ObjectiveVM();
ovm2.obList = ovm;
ovm2.DatePeriod = new DateTime(2013, 8,1);
return View(ovm2);
}
ViewModel:
public class ObjectiveVM
{
public DateTime DatePeriod { get; set; }
public IList<ObList> obList { get; set; }
public class ObList
{
public int ObjectiveId { get; set; }
public int AnalystId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string AnalystName { get; set; }
public bool Include { get; set; }
}
}
CreateMap:
Mapper.CreateMap<Objective, ObjectiveVM.ObList>()
.ForMember(dest => dest.Include, opt => opt.Ignore())
.ForMember(dest => dest.AnalystName, opt => opt.MapFrom(y => (y.Analyst.AnalystName)))
;
If I've mis-understood any advice, and you provided the answer, please post it - and I'll mark it as such.
Thank you,
Mark
As the commenter nemesv has rightly mentioned, the problem is about
ovm.ObList = Mapper.Map<IList<Objective>, IList<ObjectiveVM>>(objectives);
ObList is not a member of ObjectiveVM so, you should change the ObjectiveVM like this:
public class ObjectiveVM
{
public DateTime DatePeriod { get; set; }
public IList<ObList> obList { get; set; }
public class ObList
{
public int ObjectiveId { get; set; }
public int AnalystId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string AnalystName { get; set; }
public bool Include { get; set; }
}
}
Update:
Controller:
public ActionResult Analyst(int id)
{
var ovm = new ObjectiveVM { DatePeriod = new DateTime(2013, 8, 1) };
var objectives = db.Objectives.Include(
o => o.Analyst).Where(x => x.AnalystId == id).ToList();
ovm.obList = Mapper.Map<IList<Objective>,
IList<ObjectiveVM.ObList>>(objectives);
return View(ovm);
}