Automapper: Do not map path - c#

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;
}

Related

Unflattening data list into a nested list using AutoMapper

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);

How do you go from List<> to class in Automapper c#

In the current example I find it easy going HeaderPayload => Header since I just use the source.Data.Values directly.
What let's me construct the other way Header => HeaderPayload?
The current config is wrong and fails in the Map call.
[Test]
public void TestAutoMapper2()
{
// arrange
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Header, HeaderPayload>()
.ForMember(dest => dest.Data, map => map.MapFrom(src => src.Values))
;
});
var a = new Header { Values = new List<string> { "foo", "bar" } };
var mapper = new Mapper(config);
// act
var b = mapper.Map<Header>(a);
// assert
Assert.IsNotNull(b.Data);
Assert.IsNotEmpty(b.Data.Values);
Assert.AreEqual("foo", b.Data.Values.FirstOrDefault());
}
public class Data
{
public List<string> Values { get; set; }
}
public class Header
{
public List<string> Values { get; set; }
}
public class HeaderPayload
{
public Data Data { get; set; }
}

automap List of complex type to List of complex type using automapper

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; }
}

Automapper ignores the ignore property for nested object

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());

Automapper overwrites missing source property on list with child objects

I have a problem with Automapper. I set up a test windows form application and below is the code. Also look at the comments after each MessageBox:
public class FirstClass
{
public string FirstProp { get; set; }
public IList<FirstClassChild> Children { get; set; }
}
public class FirstClassChild
{
public string FirstChildProp { get; set; }
}
public class SecondClass
{
public string FirstProp { get; set; }
public string SecondProp { get; set; }
public IList<SecondClassChild> Children { get; set; }
}
public class SecondClassChild
{
public string FirstChildProp { get; set; }
public string SecondChildProp { get; set; }
}
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
AutoMapper.Mapper.CreateMap<FirstClass, SecondClass>();
AutoMapper.Mapper.CreateMap<FirstClassChild, SecondClassChild>();
var f = new FirstClass { FirstProp = "FirstClass" };
f.Children = new List<FirstClassChild> { new FirstClassChild { FirstChildProp = "FirstClass" } };
var s = new SecondClass { FirstProp = "SecondClass", SecondProp = "SecondClass" };
s.Children = new List<SecondClassChild> { new SecondClassChild { FirstChildProp = "SecondClass", SecondChildProp = "SecondClass" } };
AutoMapper.Mapper.Map(f, s);
var fc = new FirstClassChild { FirstChildProp = "FirstClass" };
var sc = new SecondClassChild { FirstChildProp = "SecondClass", SecondChildProp = "SecondClass" };
AutoMapper.Mapper.Map(fc, sc);
MessageBox.Show(sc.FirstChildProp);//FirstClass as expected
MessageBox.Show(sc.SecondChildProp);//SecondClass as expected
MessageBox.Show(s.FirstProp);//FirstClass as expected
MessageBox.Show(s.SecondProp);//SecondClass as expected
MessageBox.Show(s.Children.First().FirstChildProp);//FirstClass as expected
MessageBox.Show(s.Children.First().SecondChildProp);//Empty not expected!!
}
}
What can I do to avoid this? Is this behavior expected?
Anyway can anyone guide me how make SecondClass childs SecondChildProp to remain "SecondClass" as it is before the mapping occurs.
I asked a similar question here and found another similar one here.
I think #PatrickSteele makes a very good point: how is AutoMapper supposed to map a source list to a dest list of existing objects, when the dest list may not necessarily bear any resemblance to the source list? i.e. "But what if one list has 3 and the other list has 5?"
If you are sure that FirstClass and SecondClass have the same number of Children, and if the FirstClass's Nth Child always corresponds to SecondClass's Nth child, you could try something like this:
Mapper.CreateMap<FirstClass, SecondClass>()
.ForMember(m => m.Children, o => o.Ignore())
.AfterMap((src, dest) =>
{
for (var i = 0; i < dest.Children.Count; i++)
Mapper.Map(src.Children[i], dest.Children[i]);
});
or if FirstChildProp is some kind of unique key:
Mapper.CreateMap<FirstClass, SecondClass>()
.ForMember(m => m.Children, o => o.Ignore())
.AfterMap((src, dest) =>
{
foreach (var dChild in dest.Children)
{
var sChild = src.Children.Single(c => c.FirstChildProp == dChild.FirstChildProp);
Mapper.Map(sChild, dChild);
}
});

Categories

Resources