I have an Order object that has a list of OrderLine objects and an OrderVm object that has a list of OrderLineVm objects. The OrderLine object has a ValueB field that does not exist in the OrderLineVm object.
The problem I have is that ValueB gets set to null by AutoMapper even though I tell it to ignore this property:
[TestFixture]
public class AutomapperDestinationIssueTest2
{
[Test]
public void OrderLineValueBShouldNotBeNull()
{
Mapper.CreateMap<OrderVm, Order>().ForMember(dest => dest.Lines, opt => opt.UseDestinationValue());
Mapper.CreateMap<OrderLineVm, OrderLine>()
.ForMember(dest => dest.ValueB, opts => opts.Ignore());
var orderVm = new OrderVm() { Id = 1 };
orderVm.Lines.Add(new OrderLineVm() { ValueA = "New ValueA"} );
var order = new Order() { Id = 1 };
order.Lines.Add(new OrderLine() { ValueA = "Old ValueA", ValueB = "Old ValueB " });
Mapper.Map(orderVm, order);
Assert.IsNotNull(order.Lines[0].ValueB); // Fails. ValueB is null here.
}
public class OrderLine
{
public string ValueA { get; set; }
public string ValueB { get; set; }
}
public class OrderLineVm
{
public string ValueA { get; set; }
}
public class Order
{
public int Id { get; set; }
public List<OrderLine> Lines { get; set; }
public Order()
{
Lines = new List<OrderLine>();
}
}
public class OrderVm
{
public int Id { get; set; }
public List<OrderLineVm> Lines { get; set; }
public OrderVm()
{
Lines = new List<OrderLineVm>();
}
}
}
What I am missing?
Add an additional mapping to your map creation, the map from list to list and it starts working.
Mapper.CreateMap<OrderVm, Order> ().ForMember(dest => dest.Lines, opt => opt.UseDestinationValue());
Mapper.CreateMap<List<OrderLineVm>,List<OrderLine>> ();
Mapper.CreateMap<OrderLineVm, OrderLine> ().ForMember (d => d.ValueB, opt => opt.Ignore());
Related
Is there any simple way to tell automapper to only create a parent object if any of its properties has been mapped?
Imagine this simplistic model
public class ShoppingCart
{
List<Item> Items { get; set; }
}
public class Item
{
string Name { get; set; }
int Price { get; set; }
}
public class Order
{
string Id { get; set; }
Pizza Pizza { get; set; }
}
public class Pizza
{
int Price { get; set; }
}
I want to map shopping cart to order. Simply said, the Order.Pizza.Price should have the value mapped from the price of an item called "pizza" in the shopping cart. If there is no item named "pizza" i want the pizza in order to be null instead of an empty/default object
So, these would be the test cases
[Test]
public void Test_WithPizza()
{
// Arrange
var cart = new ShoppingCart()
{
Items = new List<Item>()
{
new Item() { Name = "Cola", Price = 10 },
new Item() { Name = "Pizza", Price = 20 }
}
};
//Act
var order = Mapper.Map<ShoppingCart, Order>(cart);
//Assert
Assert.IsNotNull(order);
Assert.IsNotNull(order.Pizza);
Assert.AreEqual(20, order.Pizza.Price);
}
[Test]
public void Test_WithoutPizza()
{
// Arrange
var cart = new ShoppingCart()
{
Items = new List<Item>()
{
new Item() { Name = "Cola", Price = 10 }
}
};
//Act
var order = Mapper.Map<ShoppingCart, Order>(cart);
//Assert
Assert.IsNotNull(order);
Assert.IsNull(order.Pizza); // Fails
}
My mapping profile looks like this. The condition does not seem to effect whether the parent object (Pizza) is instantiated or not.
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<ShoppingCart, Order>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(_ => Guid.NewGuid()))
.ForPath(dest => dest.Pizza.Price, opt =>
{
opt.MapFrom(src => GetPizzaPrice(src));
opt.Condition(con => GetPizzaPrice(con.Source) != null);
});
}
private static int? GetPizzaPrice(ShoppingCart cart)
=> cart.Items.SingleOrDefault(i => i.Name == "Pizza")?.Price;
}
I have a flatten data list (counties and cities) and I want to map into a unflatten (counties with cities) nested property list (State.County). Please check the following .NET 6.0 console application code:
using AutoMapper;
public class CountyAndCity
{
public int CountyId { get; set; }
public string CountyName { get; set; }
public int CityId { get; set; }
public string CityName { get; set; }
public int CityPopulation { get; set; }
}
public class City
{
public int Id { get; set; }
public string Name { get; set; }
public int Population { get; set; }
}
public class County
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<City> Cities { get; set; }
}
public class State
{
public List<County> Counties { get; set; }
}
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap<CountyAndCity, City>()
.ForMember(destination => destination.Id, options => options.MapFrom(source => source.CountyId))
.ForMember(destination => destination.Name, options => options.MapFrom(source => source.CountyName))
.ForMember(destination => destination.Population, options => options.MapFrom(source => source.CityPopulation));
CreateMap<IEnumerable<CountyAndCity>, County>()
.ForMember(destination => destination.Id, options => options.MapFrom(source => source.FirstOrDefault().CountyId))
.ForMember(destination => destination.Name, options => options.MapFrom(source => source.FirstOrDefault().CountyName))
.ForMember(destination => destination.Cities, options => options.MapFrom(source => source));
CreateMap<IEnumerable<CountyAndCity>, State>()
.ForMember(destination => destination.Counties, options => options.MapFrom(source => source));
}
}
class HelloWorld
{
static void Main()
{
var flattenData = new List<CountyAndCity>
{
new CountyAndCity { CountyId = 1, CountyName = "Albany", CityId = 10, CityName = "Cohoes", CityPopulation = 1000 },
new CountyAndCity { CountyId = 1, CountyName = "Albany", CityId = 20, CityName = "Watervliet", CityPopulation = 1000 },
new CountyAndCity { CountyId = 2, CountyName = "Bronx", CityId = 30, CityName = "Wakefield", CityPopulation = 2000 },
new CountyAndCity { CountyId = 2, CountyName = "Bronx", CityId = 40, CityName = "Eastchester", CityPopulation = 2000 },
};
var unflattenData = new List<County>
{
new County { Id = 1, Name = "Albany", Cities = new List<City>
{
new City { Id = 10, Name = "Cohoes", Population = 1000 },
new City { Id = 20, Name = "Watervliet", Population = 1000 },
}},
new County { Id = 2, Name = "Bronx", Cities = new List<City>
{
new City { Id = 30, Name = "Wakefield", Population = 2000 },
new City { Id = 40, Name = "Eastchester", Population = 2000 },
}},
};
var mapperConfiguration = new MapperConfiguration(configuration => configuration.AddProfile(new AutoMapperProfile()));
var mapper = mapperConfiguration.CreateMapper();
State mappedData = mapper.Map<State>(flattenData);
//Should be TRUE after some kind of custom comparison but it is goof enough for the example.
bool result = mappedData.Counties == unflattenData;
}
}
I'm receiving the following exception:
/*
Error mapping types.
Mapping types:
IEnumerable`1 -> State
System.Collections.Generic.IEnumerable`1[[CountyAndCity, ConsoleApp1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] -> State
Type Map configuration:
IEnumerable`1 -> State
System.Collections.Generic.IEnumerable`1[[CountyAndCity, ConsoleApp1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] -> State
Destination Member:
Counties
*/
What I'm doing wrong? What mappers should I create to transform the unflatten list into the flatten nested property: "Counties"?
You may look for the Custom Value Resolver to group data for County.
public class StateCountiesResolver : IValueResolver<List<CountyAndCity>, State, List<County>>
{
public List<County> Resolve(List<CountyAndCity> src, State dest, List<County> destMember, ResolutionContext ctx)
{
destMember = src.GroupBy(x => x.CountyId)
.Select(x => new County
{
Id = x.Key,
Name = x.FirstOrDefault()?.CountyName,
Cities = ctx.Mapper.Map<List<City>>(x.ToList())
})
.ToList();
return destMember;
}
}
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap<CountyAndCity, City>()
.ForMember(destination => destination.Id, options => options.MapFrom(source => source.CityId))
.ForMember(destination => destination.Name, options => options.MapFrom(source => source.CityName))
.ForMember(destination => destination.Population, options => options.MapFrom(source => source.CityPopulation));
CreateMap<List<CountyAndCity>, State>()
.ForMember(destination => destination.Counties, options => options.MapFrom<StateCountiesResolver>());
}
}
Demo # .NET Fiddle
While with the below statement:
bool result = mappedData.Counties == unflattenData;
you will still get false when comparing both lists (even the contents of both lists are the same) as the references are used to compare instead of the content of both lists:
Instead, you need to override the Equals() and GetHashCode() methods.
public class City
{
public int Id { get; set; }
public string Name { get; set; }
public int Population { get; set; }
public override bool Equals (Object obj)
{
if (obj is not City)
return false;
City b = (City)obj;
return Id == b.Id
&& Name == b.Name
&& Population == b.Population;
}
public override int GetHashCode()
{
return this.Id.GetHashCode() ^ this.Name.GetHashCode() ^ this.Population.GetHashCode();
}
}
public class County
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<City> Cities { get; set; }
public override bool Equals (Object obj)
{
if (obj is not County)
return false;
County b = (County)obj;
return Id == b.Id
&& Name == b.Name
&& Enumerable.SequenceEqual(Cities, b.Cities);
}
public override int GetHashCode()
{
return this.Id.GetHashCode() ^ this.Name.GetHashCode();
}
}
And use Enumerable.SequenceEqual() method to compare the contents of the lists.
bool result = Enumerable.SequenceEqual(mappedData.Counties, unflattenData);
If the header object has a prop set to 1 then it should map field type1 in the child to type in the destination. Otherwise it should use type2.
Bonus points if I can use IValueResolver to use type1 or type1extended if extended is filled.
Here is my minimum viable product/demo
using AutoMapper;
using AutoMapper.Configuration.Conventions;
using System;
using System.Collections.Generic;
namespace ConsoleAppAutoMapper
{
class Program
{
static void Main(string[] args)
{
var source = new SourceParent() {
Header = new SourceHeader() { Currency = 30, FileName = "testfile.txt", Type = 1 },
Rows = new List<SourceRow>() {
new SourceRow() { ID = 1, Amount1 = 100, Amount2 = 200 },
new SourceRow() { ID = 2, Amount1 = 101, Amount2 = 201 },
new SourceRow() { ID = 3, Amount1 = 102, Amount2 = 202 }
} };
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<SourceParent, DestinationParent>();
cfg.CreateMap<SourceRow, DestinationRow>()
.ForMember(x => x.Type, opt => opt.MapFrom(p => p.Type1));
});
var mapper = config.CreateMapper();
var dest = mapper.Map<DestinationParent>(source);
Console.WriteLine(dest.Rows[0].Type == 100); // should be true if SourceHeader.Type = 1 and should be 200 (SourceRow.Type2) if SourceHeader.Type = 2
Console.ReadKey();
}
}
// source
public class SourceParent
{
public SourceHeader Header { get; set; }
public List<SourceRow> Rows { get; set; }
}
public class SourceHeader
{
public string FileName { get; set; }
public int Type { get; set; }
}
public class SourceRow
{
public int ID { get; set; }
public int Amount1 { get; set; }
public int Amount2 { get; set; }
}
//destination
public class DestinationParent
{
public DestinationHeader Header { get; set; }
public List<DestinationRow> Rows { get; set; }
}
public class DestinationHeader
{
public string FileName { get; set; }
}
public class DestinationRow
{
public int ID { get; set; }
public int Type { get; set; }
public int Amount{ get; set; } // if type=1 then source is amount1 otherwise amount2
}
}
edit
I tried to solve it by having an Aftermap on the sourceparent mapping which took the value from the header and put it in a prop from the destinationrow (it is the Type value) and wanted another aftermap on the row to see if I needed prop A or B (type1 or type2) but that aftermap still does not know (it's null) what type it is because it happens before the aftermap of the parent it seems.
public class MapRowType : IMappingAction<SourceParent, DestinationParent>
{
public void Process(SourceParentsource, DestinationParent destination)
{
foreach (var row in destination.Rows)
{
row.Type = source.Header.Type; // so now I have type in the row, but still do not know if I should use Amount1 or Amount2
}
}
}
you can use the resolution context. Declare the mapping:
cfg.CreateMap<SourceRow, DestinationRow>()
.ForMember(x => x.Type,
opt => opt.ResolveUsing((src, dest1, destMember, resContext) => resContext.Items["Type"] as int? == 1? src.Type2: src.Type1));
After pass the value:
var dest = mapper.Map<DestinationParent>(source, opts=> { opts.Items["Type"] = source.Header.Type;});
I am trying to map source string to List<T> for member but the value of the mapped member list property is always null.
Here is my code.
//Source model
public class Claims
{
public int Id { get; set; }
public string ClaimType {get; set; }
public string ClaimValue { get; set; } // produce json string [{"Action":"read","Status":"active"}]
}
// Dest model
public class ClaimsDto
{
public int Id { get; set; }
public string ClaimType { get; set; }
public List<ResourceActions> ClaimValues { get; set; }
}
public class ResourceActions
{
public string Action { get; set; }
public string Status { get; set; }
}
CreateMap<ResourceActions, ResourceActions>();
CreateMap<Claims, ClaimsDto>()
.ForMember(dest => dest.ClaimValues,
opt => opt.MapFrom( src=> JsonConvert.DeserializeObject<List<ResourceActions>>(src.ClaimValue)))
.ReverseMap();
Also tried with resolver like this.
.ForMember(dto => dto.ClaimValues, opt => opt.ResolveUsing<CustomResolver,string>(src=>src.ClaimValue ))
The resolver:
public class CustomResolver : IMemberValueResolver<Claims, ClaimsDto, string, List<ResourceActions>>
{
public List<ResourceActions> Resolve(Claims source, ClaimsDto destination, string sourceMember, List<ResourceActions> destinationMember, ResolutionContext context)
{
var data = JsonConvert.DeserializeObject<List<ResourceActions>>(sourceMember);
return context.Mapper.Map<List<ResourceActions>>(data); // not working
return JsonConvert.DeserializeObject<List<ResourceActions>>(sourceMember); // not working
}
}
And the controller
var mapped = _mapper.Map<List<ClaimsDto>>(source); // success without exception but the list property is null.
Other properties are mapped except this string to List<T> property.
I think this profile should solve the issue:
public class MyMapperProfile : Profile
{
public MyMapperProfile()
{
CreateMap<Claims, ClaimsDto>()
.ForMember(dto => dto.ClaimValues, cfg => cfg.MapFrom((claim, _) =>
JsonSerializer.Deserialize<IReadOnlyCollection<ResourceActions>>(claim.ClaimValue)));
}
}
Here is an example:
var config = new MapperConfiguration(cfg => cfg.AddProfile<MyMapperProfile>());
var mapper = config.CreateMapper();
var someClaims = new List<Claims>
{
new Claims { Id = 5, ClaimType = "Foo", ClaimValue = #"[{""Action"":""read"",""Status"":""active""}]" },
new Claims { Id = 7, ClaimType = "Bar", ClaimValue = #"[{""Action"":""create"",""Status"":""disabled""}]" },
};
var result = mapper.Map<List<ClaimsDto>>(someClaims);
foreach (var item in result)
{
Console.WriteLine(JsonSerializer.Serialize(item));
}
public class SourceExamModel
{
public int ExamId { get; set; }
public List<SectionModel> Sections { get; set; }
}
public class DesiationExamModel
{
public in ExamId {get;set;}
public System.Collections.Generic.IEnumerable<SectionModel> DestSections
{
get
{
}
set
{
}
}
What I tried:
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<CrmMapper.SourceExamModel, CrmMapper.DestiationExamModel>()
.ForMember(v => v.Id, opts => opts.MapFrom(src => src.Id))
.ForMember(v => v.DestSections, opts => opts.MapFrom(src => src.SourceSections));
});
IMapper mapper = config.CreateMapper();
var source = new ExamModel();
var dest = mapper.Map<SourceExamModel, CrmMapper.DestiationExamModel>(source);
Can ayone help me how to map list of comple objects o los of complex objects
Assuming half of your example code is simple spelling mistakes, you pretty much have it working.
If the properties on the source and destination are named the same, you don't have to explicitly map them.
Your source in your example doesn't make sense, it needs the correct object and data.
Here's my attempt at a working example you can copy past into a console application.
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg =>
cfg.CreateMap<SourceExamModel, DestinationExamModel>()
.ForMember(dest => dest.DestSections, c => c.MapFrom(src => src.Sections))
);
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
var source = new SourceExamModel
{
ExamId = 1,
Sections = new List<SectionModel> { new SectionModel { SectionId = 1 }, new SectionModel { SectionId = 2 } }
};
var destination = mapper.Map<SourceExamModel, DestinationExamModel>(source);
}
}
public class SourceExamModel
{
public int ExamId { get; set; }
public List<SectionModel> Sections { get; set; }
}
public class DestinationExamModel
{
public int ExamId { get; set; }
public List<SectionModel> DestSections { get; set; }
}
public class SectionModel
{
public int SectionId { get; set; }
}