Automapper ignores the mapping for derived class - c#

I have a class hierarchy as described below:
public partial class EmployeeDTO
{
public virtual long Id { get; set; }
public virtual string Name { get; set; }
public virtual string ReportsTo { get; set; }
}
public class ManagerDTO: EmployeeDTO
{
public override string ReportsTo
{
get => null;
set => throw new InvalidOperationException("ReportsTo can not be set to other than the preset value.");
}
}
public class EmployeeModel
{
public long Id { get; set; }
public string FullName { get; set; }
public EmployeeType Type { get; set; }
public string ReportsTo { get; set; }
}
public enum EmployeeType
{
Employee = 0,
Manager = 1,
}
Mapping configuration
CreateMap<EmployeeDTO, EmployeeModel>()
.ForMember(empmodel => empmodel.FullName, empdto => empdto.MapFrom(empdto => empdto.Name))
.ForMember(empmodel => empmodel.Type, empdto => empdto.MapFrom(empdto => EmployeeType.Employee));
CreateMap<ManagerDTO, EmployeeModel>()
.ForMember(model => model.FullName, dto => dto.MapFrom(dto => dto.Name))
.ForMember(model => model.Type, dto => dto.MapFrom(dto => EmployeeType.Manager));
CreateMap<EmployeeModel, ManagerDTO>()
.ForMember(dto => dto.Name, model => model.MapFrom(model => model.FullName))
.ForMember(t => t.ReportsTo, t => t.Ignore());
CreateMap<EmployeeModel, EmployeeDTO>()
.ForMember(dto => dto.Name, model => model.MapFrom(model => model.FullName));
I am trying to patch the entity as follows:
[HttpPatch]
public IActionResult Patch([FromBody] Delta<EmployeeDTO> entityDelta)
{
var existingModel = new EmployeeModel
{
Id = 123,
FullName = "XYZ",
Type = Shared.EmployeeType.Manager,
ReportsTo = null
};
var existingEntity = _mapper.Map<EmployeeDTO>(existingModel);
entityDelta.Patch(existingEntity);
// Following mapping incorrectly sets type of updatedModel as EmployeeType.Employee
var updatedModel = _mapper.Map(existingEntity, existingModel);
return Ok(updatedModel);
}
After executing the above code expected type of the updatedModel to be Manager, but the type is Employee instead.
I have tried using the other overloads for the Map. Map(entity) works as expected when explicit type is provided for the previous mapping ie. Map(model). However, this overload creates another instance of the dbmodel with the same id, which leads to further issues.
To avoid having two runtime DbEntries with the same id, I am trying to use the Map(entity, existimgModel) overload of the mapper, which is failing to give the desired outcome.
Any ideas on how to solve this?
Sample code

Related

AutoMapper Ignore the Mapping Conditionally when the List type of property in ViewModel is null

I have the view model as below:
public class CarViewModel
{
public Guid Id { get; set; }
public string CarName { get; set; }
public List<EngineTypeViewModel> Engine { get; set; }
}
public class EngineTypeViewModel
{
public int Id { get; set; }
public string name { get; set; }
}
Entity:
public class Car
{
public Guid Id { get; set; }
public string CarName { get; set; }
public virtual ICollection<EngineType> Engine { get; set; }
}
public class EngineType
{
public int Id { get; set; }
public string name { get; set; }
}
I wanted to Map the CarViewModel to Car Entity types as below:
CreateMap<CarViewModel, Car>()
.ForMember(dest => dest.Engine, src => src.Ignore());
In the mapping configuration I wanted to map from CarViewModel to Car.
But if the engine count is 0 then the mapped should be ignore.
CreateMap<CarViewModel, Car>()
.ForMember(dest => dest.Engine, src => src.Ignore());
//Map only when src.Engine.Count > 0 other wise Ignore
// What should be my approach here??
This means during the update when i pull using GetById. Like
public Update(CarViewModel model)
{
var car = obj.GetById(model.Id);
var mapped = map.Map(model,car);
//if the model.Engine.Count = 0 car.Engine
//should be same as mapped.Engine, there should not be mapping with the
//object(Engine) from the `CarViewModel`
}
Is there any extension in Auto mapper to this task. I have tried as above src.Ignore() it is going to ignore during the ADD also.
So, I do not want to MAP the null value during the Update. Or, it should remain as the destination value
Edit:
AutoMapper Configuration:
CreateMap<CarViewModel, Car>().ForMember(dest => dest.Engine, o =>
{
o.Condition(src => src.Engine.Count > 0);
//mapping
o.MapFrom(src => src.Engine);
});
Mapping Records:
var carmodel = new CarViewModel();
carmodel.CarName = "Maruti";
carmodel.Id = Guid.NewGuid();
var carentity = new Car();
carentity.CarName = "Maruti";
carentity.Id = Guid.NewGuid();
carentity.Engine.Add(new EngineType { Id = 1, name = "PetrolEngine" });
carentity.Engine.Add(new EngineType { Id = 2, name = "DieselEngine" });
carentity.Engine.Add(new EngineType { Id = 3, name = "CranotEngine" });
var mapped = _mapper.Map(carmodel, carentity);
You can look at the mapped. mapped.Engine is Empty
Expected Output:

How to map a simple poco into a complex object hierachy using automapper?

I have a simple poco that need to be mapped to an object supplied by a third party that uses a complex object hierarchy. I would like to use AutoMapper if possible but I am unsure how to set it up correctly.
I have supplied a simplified example below to show what I am trying to do.
My poco:
public class Person
{
public string FirstName { get; set; }
public string Lastname { get; set; }
public string FullName { get { return Firstname + " " + Lastname; } }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string City { get; set; }
public string Country { get; set; }
public string Postcode { get; set; }
public string Reference { get; set; }
}
Third Party objects
public class People
{
public Person[] Person { get; set; }
}
public class Person
{
public Names Names { get; set; }
public Address Address { get; set; }
public ReasonsForRequest[] Reasons { get; set; }
}
public class Names
{
public string Fullname { get; set; }
}
public class Address
{
public string AddressLine[] { get; set; }
public string City { get; set; }
public string Country { get; set; }
public string Postcode { get; set; }
}
public class ReasonsForRequest
{
public StructuredReasons StructuredReasons { get; set; }
}
public class StructuredReasons
{
public Reference Ref { get; set; }
}
public class Reference
{
public string Ref { get; set; }
}
The issue I am having is Registering the mappings to get the reference mapped. How can I register a mappig to an object that just contains an object when I need to map to the inner object and to a parent object?
[For clarity, I'm going to call your Person POCO PersonDTO, since the 3rd party code also has a class called Person.]
There are a couple of ways of doing this. One, which I've used in the past, involves setting up a mapping from PersonDTO to Names, another from PersonDTO to Address, and another from PersonDTO to Reasons. Finally, you add a mapping from PersonDTO to Person. It looks like this (I've left out Reasons, but you get the idea):
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<PersonDTO, Names>()
.ForMember(d => d.Fullname, o => o.MapFrom(s => s.FullName));
cfg.CreateMap<PersonDTO, Address>()
.ForMember(d => d.AddressLine,
o => o.MapFrom(s => new[] { s.AddressLine1, s.AddressLine2 }));
cfg.CreateMap<PersonDTO, Person>()
.ForMember(d => d.Names, o => o.MapFrom(s => s))
.ForMember(d => d.Address, o => o.MapFrom(s => s));
});
var mapper = config.CreateMapper();
var myPerson = new PersonDTO() {
FirstName = "Bob",
LastName = "Gold",
AddressLine1 = "123 Main Street",
AddressLine2 = "Apt. 2"
};
var theirPerson = mapper.Map<Person>(myPerson);
But a recent version of AutoMapper added a ForPath() method which makes all of this simpler by letting you access inner objects. Now the code collapses to this:
var config = new MapperConfiguration(cfg =>
cfg.CreateMap<PersonDTO, Person>()
.ForPath(d => d.Names.Fullname, o => o.MapFrom(s => s.FullName))
.ForPath(d => d.Address.AddressLine,
o => o.MapFrom(s => new[] { s.AddressLine1, s.AddressLine2 }))
);
var mapper = config.CreateMapper();
Edit: I left out one aspect which may change the balance between these two methods. In the first method, with multiple maps defined, you get for free any fields with matching names. For example, you don't need to explicitly map PesronDTO.City to Address.City (and if you change the case on PersonDTO.FullName to be PersonDTO.Fullname, you'd get that for free as well). But in the second method, you have to explicitly map all nested fields, even if the names match. In your case, I think the first method would probably work better for you, because you'd have to map only 3 fields explicitly and would get the other 4 for free. For the second method, you'd have to do a ForPath() for all 7 fields.
By default it tries to match the properties of the SourceType to those of the DestinationType. But for your scenario you'll have add some specifications using the ForMember extension method, like this:
Mapper.CreateMap<Foo, FooDTO>()
.ForMember(e => e.Bars, o => o.ExplicitExpansion());
There's an existing thread on this one here:
AutoMapper define mapping level

Automapper projection and union

I have a problem with union and automapper projections.
I have two entities:
public class Entity
{
public DateTime ActionDate { get; set; }
public int SomeProp { get; set; }
}
public class ExtendedEntity
{
public DateTime ActionDate { get; set; }
public int SomeProp { get; set; }
public int SomeOtherProp { get; set; }
}
and projection:
public class EntityProjection
{
public DateTime ActionDate { get; set; }
public int SomeProp { get; set; }
public int SomeOtherProp { get; set; }
public string Source { get; set; }
}
i map entities to one projection, Entity does not have SomeOtherProp so i set 0 to it:
public class EntityProfile : Profile
{
protected override void Configure()
{
CreateMap<ExtendedEntity, EntityProjection>()
.ForMember(dest => dest.Source, opt => opt.MapFrom(src => "ext entity"));
CreateMap<Entity, EntityProjection>()
.ForMember(dest => dest.SomeOtherProp, opt => opt.MapFrom(src => 0))
.ForMember(dest => dest.Source, opt => opt.MapFrom(src => "entity"));
}
}
when i try to use next code i get error:
var entities = context.Set<Entity>()
.Project().To<EntityProjection>();
var extEntities = context.Set<ExtendedEntity>()
.Project().To<EntityProjection>();
var result = entities.Union(extEntities).OrderBy(p => p.ActionDate).ToList();
Error text: The type 'UserQuery+EntityProjection' appears in two structurally incompatible initializations within a single LINQ to Entities query. A type can be...
That means that properties in projection must be initialized in same order, how i can set projection properties initialization order by automapper?
Very late answer, and the short version seems to be "You can't".
I had exactly the same question (Can I force Automapper to initialise properties in a certain order?) and ended up mapping everything within a LINQ select statement.
For ease, I made it a static method within my DTO (cut-down code):
public static IQueryable<MyDto> QueryableFromTaskType1(
IQueryable<TaskType1> query)
{
return query.Select(src => new MyDto()
{
TaskId = src.Id,
AssetTypeName = src.Asset.AssetType.Name,
AssetId = src.Asset.Id,
AssetCode = src.Asset.Code,
AssetName = src.Asset.Name,
});
}
public static IQueryable<MyDto> QueryableFromTaskType2(
IQueryable<TaskType2> query)
{
return query.Select(src => new MyDto()
{
TaskId = src.Id,
AssetTypeName = src.AssetTypeName,
AssetId = src.AssetId,
AssetCode = src.AssetCode,
AssetName = src.AssetName,
});
}
then you can get your objects, as an IQueryable, simply pass them through the appropriate static method (which appends a select into the DTO - or projects as it's otherwise known) and then Union or Concat the resulting IQueryables.
The only downside is that Automapper will normally deal with recursive automapping, although I'm pretty certain that wouldn't map to SQL well anyway, so you probably don't lose much.

Automapper Project.To breaks db IQueryable

I have entities:
[Table("Representatives")]
public class Representative
{
[Column("RepID"), Key]
public int Id { get; set; }
[Column("MemID")]
public int MemberId { get; set; }
public virtual Member Member { get; set; }
}
[Table("Members")]
public class Member
{
[Column("MemID"), Key]
public int Id { get; set; }
[ForeignKey("MemberId")]
public virtual ICollection<Representative> Representatives { get; set; }
}
where Representatives relate to Member as many to one. I want to retrieve some representatives with their parent members and project it all to some view model. I've configured automapper like this:
Mapper.CreateMap<Representative, RepresentativeViewModel>()
.ForMember(m => m.RepId, a => a.MapFrom(x => x.Id))
.ForMember(m => m.MemberModel, a => a.MapFrom(x => x.Member));
Mapper.CreateMap<Member, MemberViewModel>()
.ForMember(m => m.MemId, a => a.MapFrom(x => x.Id))
.ForMember(m => m.OrganizationName, a => a.MapFrom(x => x.Name));
If I run this code:
var prj = ctx.Representatives.Where(r => r.Id == 1).Project().To<RepresentativeViewModel>();
then I get expression tree that contains func invocation (debugged):
...
.Lambda #Lambda2<System.Func`2[ConsoleApplication1.Representative,ConsoleApplication1.RepresentativeViewModel]>(ConsoleApplication1.Representative $var1)
{
.New ConsoleApplication1.RepresentativeViewModel(){
Name = $var1.Name,
RepId = $var1.Id,
MemberModel = .Invoke (.Lambda #Lambda3<System.Func`2[ConsoleApplication1.Member,ConsoleApplication1.MemberViewModel]>)($var1.Member)
}
}
.Lambda #Lambda3<System.Func`2[ConsoleApplication1.Member,ConsoleApplication1.MemberViewModel]>(ConsoleApplication1.Member $var2)
{
.New ConsoleApplication1.MemberViewModel(){
MemId = $var2.Id,
OrganizationName = $var2.Name
}
}
...
so for some reason builder invokes Func instead of calling Expression. Do you have ideas why it's so? How can I make my mapper build correct expression and SQL query?

Automapper not ignoring nested property

I've had a look through the various similar posts but can't spot the error of my ways with this one. Basically I have two views which update different parts of a "Settings" object. The view models contain one of two properties and depending on which is being set, should ignore the other. It works fine when it's ViewModel ==> Entity direct but when Automapper tries to update the nested object it fails.
I have the following object structure:
public class Account
{
public int AccountId { get; set; }
public DateTime DateToBeIgnored { get; set; }
public AccountSetting Settings { get; set; }
}
public class AccountSetting
{
public string PropertyOne { get; set; }
public string PropertyTwo { get; set; }
}
public class AccountViewModel
{
public int AccountId { get; set; }
public DateTime DateToBeIgnored { get; set; }
public AccountSettingViewModel Settings { get; set; }
}
public class AccountSettingViewModel
{
public string PropertyTwo { get; set; }
}
public class OtherAccountSettingViewModel
{
public string PropertyOne { get; set; }
}
With the mappings:
void WireUpMappings()
{
// From the entities to the view models
Mapper.CreateMap<Account, AccountViewModel>();
Mapper.CreateMap<AccountSetting, AccountSettingViewModel>();
Mapper.CreateMap<AccountSetting, OtherAccountSettingViewModel>();
// From the view models to the entities
Mapper.CreateMap<AccountViewModel, Account>()
.ForMember(dest => dest.DateToBeIgnored, opt => opt.Ignore());
Mapper.CreateMap<AccountSettingViewModel, AccountSetting>()
.ForMember(dest => dest.PropertyTwo, opt => opt.Ignore());
Mapper.CreateMap<OtherAccountSettingViewModel, AccountSetting>()
.ForMember(dest => dest.PropertyOne, opt => opt.Ignore());
Mapper.AssertConfigurationIsValid();
}
When mapping [OtherAccountSettingViewModel --> AccountSetting], only the property "PropertyTwo" is assigned (and the original value for "PropertyOne" remains unchanged) -this is what I would expect.
However when mapping [AccountViewModel --> Account] "DateToBeIgnored" is ignored as intended whereas Account.AccountSetting.PropertyTwo's previous value is replaced with "null".
Can anyone spot the error of my ways?
Here is the solution:
[TestMethod]
public void TestMethod1()
{
Mapper.CreateMap<AccountViewModel, Account>()
.ForMember(dest => dest.DateToBeIgnored, opt => opt.Ignore())
.ForMember(dest=>dest.Settings, opt=>opt.UseDestinationValue());
Mapper.CreateMap<AccountSettingViewModel, AccountSetting>()
.ForMember(dest=>dest.PropertyOne, opt=>opt.Ignore())
.ForMember(dest => dest.PropertyTwo, opt => opt.MapFrom(a => a.PropertyTwo));
Mapper.AssertConfigurationIsValid();
AccountViewModel viewmodel = new AccountViewModel()
{
AccountId = 3,
DateToBeIgnored = DateTime.Now,
Settings = new AccountSettingViewModel() { PropertyTwo = "AccountSettingViewModelPropTwo" }
};
Account account = new Account()
{
AccountId = 10,
DateToBeIgnored = DateTime.Now,
Settings = new AccountSetting() { PropertyOne = "AccountPropOne", PropertyTwo = "AccountPropTwo" }
};
account = Mapper.Map<AccountViewModel, Account>(viewmodel, account);
Assert.IsNotNull(account);
}

Categories

Resources