AutoMapper and flattening nested arrays - c#

I'm trying to use AutoMapper to flatten multiple levels of arrays.
Consider the following source classes:
class X {
public string A { get; set; }
public Y[] B { get; set; }
}
class Y {
public string C { get; set; }
public Z[] D { get; set; }
}
class Z {
public string E { get; set; }
public string F { get; set; }
}
And the following destination:
class Destination {
public string A { get; set; }
public string C { get; set; }
public string E { get; set; }
public string F { get; set; }
}
What I'd like to be able to do is get a List from one or more X, e.g.:
Mapper.Map<IEnumerable<X>, IEnumerable<Destination>>(arrayOfX);
I'm unable to figure out what sort of mapping configuration to use to achieve this. MapFrom seems like the way to go for 1:1 compositions, but doesn't seem to be able to handle the array (or other enumerable) unless I use AutoMapper's destination naming convention.
Any insights on how to achieve this?

Try this mapper,
Mapper.CreateMap<Z, Destination>();
Mapper.CreateMap<Y, Destination>();
Mapper.CreateMap<X, Destination>()
.ForMember(destination => destination.A, options => options.MapFrom(source => source.A)).IgnoreAllNonExisting()
.ForMember(destination => destination.C, options => options.MapFrom(source => Mapper.Map<IEnumerable<Y>, IEnumerable<Destination>>(source.B).FirstOrDefault().C))
.ForMember(destination => destination.E, options => options.MapFrom(source => Mapper.Map<IEnumerable<Z>, IEnumerable<Destination>>(source.B.SelectMany(d => d.D)).FirstOrDefault().E))
.ForMember(destination => destination.F, options => options.MapFrom(source => Mapper.Map<IEnumerable<Z>, IEnumerable<Destination>>(source.B.SelectMany(d => d.D)).FirstOrDefault().F));
var result = Mapper.Map<IEnumerable<X>, IEnumerable<Destination>>(arrayOfX);

I had a very similar problem a while ago. I had a collection of locations, and each location had a collection of streets. I wanted to map them to a collection of view models where each view model represented a street (including the location details).
This was my solution: https://groups.google.com/forum/#!topic/automapper-users/b66c1M8eS8E
For this particular problem, this could be your mapping configuration:
public static class AutoMapperConfig
{
public static void Configure()
{
Mapper.CreateMap<Z, Destination>()
.ForMember(dest => dest.A, opt => opt.Ignore())
.ForMember(dest => dest.C, opt => opt.Ignore());
Mapper.CreateMap<Y, Destination>()
.ForMember(dest => dest.A, opt => opt.Ignore())
.ForMember(dest => dest.E, opt => opt.Ignore())
.ForMember(dest => dest.F, opt => opt.Ignore());
Mapper.CreateMap<X, Destination>()
.ForMember(dest => dest.C, opt => opt.Ignore())
.ForMember(dest => dest.E, opt => opt.Ignore())
.ForMember(dest => dest.F, opt => opt.Ignore());
}
}
Because AutoMapper is primarily a 1:1 mapping, you need to implement a wee bit of magic to map to multiple objects. This is an example of how you could call that mapping to populate your object:
var rc = data.SelectMany(
x => x.B.SelectMany(
y => y.D
.Select(Mapper.Map<Z, Destination>)
.Select(z => Mapper.Map(y, z))
)
.Select(y => Mapper.Map(x, y))
);
Here are a couple of unit tests to validate the mapping and show it in action:
[TestFixture]
public class MapperTests
{
[Test]
public void Mapping_Configuration_IsValid()
{
AutoMapperConfig.Configure();
Mapper.AssertConfigurationIsValid();
}
[Test]
public void Mapping_TestItems_MappedOK()
{
AutoMapperConfig.Configure();
Mapper.AssertConfigurationIsValid();
var data = new[]
{
new X
{
A = "A1",
B = new[]
{
new Y
{
C = "A1C1",
D = new[]
{
new Z
{
E = "A1C1E1",
F = "A1C1F1"
},
new Z
{
E = "A1C1E2",
F = "A1C1F2"
},
}
},
new Y
{
C = "A1C2",
D = new[]
{
new Z
{
E = "A1C2E1",
F = "A1C2F1"
},
new Z
{
E = "A1C2E2",
F = "A1C2F2"
},
}
}
}
}
};
var rc = data.SelectMany(
x => x.B.SelectMany(
y => y.D
.Select(Mapper.Map<Z, Destination>)
.Select(z => Mapper.Map(y, z))
)
.Select(y => Mapper.Map(x, y))
);
Assert.That(rc, Is.Not.Null);
Assert.That(rc.Count(), Is.EqualTo(4));
var item = rc.FirstOrDefault(x => x.F == "A1C2F2");
Assert.That(item, Is.Not.Null);
Assert.That(item.A, Is.EqualTo("A1"));
Assert.That(item.C, Is.EqualTo("A1C2"));
Assert.That(item.E, Is.EqualTo("A1C2E2"));
Assert.That(item.F, Is.EqualTo("A1C2F2"));
}
}

For anyone else coming across this post by searching how to Flatten object structure with AutoMapper - the new AutoMapper supports flattening with IncludeMembers() syntax.
Source: http://docs.automapper.org/en/stable/Flattening.html
So the original issue could be solved like this:
Mapper.CreateMap<Z, Destination>();
Mapper.CreateMap<Y, Destination>().IncludeMembers(src => src.D);
Mapper.CreateMap<X, Destination>().IncludeMembers(src => src.B);

Related

C# AutoMapper issue with mapping Many-to-Many, wrong foreign key

I have faced with the strange issue with mapping model to entity with Many-to-Many relationship
I have the following entities and models:
using System;
using System.Diagnostics;
using System.Collections.Generic;
namespace AutoMapper.ReproducedExample
{
public class StoreEntity
{
public int Id { get; set; }
public string Name { get; set; }
public List<StoreProductEntity> Products { get; set; } = new List<StoreProductEntity>();
}
public class ProductEntity
{
public int Id { get; set; }
public string Name { get; set; }
public List<StoreProductEntity> Stores { get; set; } = new List<StoreProductEntity>();
}
public class StoreProductEntity
{
public int StoreId { get; set; }
public StoreEntity Store { get; set; }
public int ProductId { get; set; }
public ProductEntity Product { get; set; }
}
public class StoreModel
{
public int Id { get; set; }
public string Name { get; set; }
public List<ProductModel> Products { get; set; } = new List<ProductModel>();
}
public class ProductModel
{
public int Id { get; set; }
public string Name { get; set; }
public List<StoreModel> Stores { get; set; } = new List<StoreModel>();
}
public class CustomProfile : Profile
{
public CustomProfile()
{
CreateMap<StoreModel, StoreEntity>()
.ForMember(d => d.Products,
opt => opt.MapFrom(s => s.Products))
.AfterMap((model, entity) =>
{
foreach (var entityProduct in entity.Products)
{
entityProduct.StoreId = entity.Id;
entityProduct.Store = entity;
}
});
CreateMap<StoreModel, StoreProductEntity>()
.ForMember(entity => entity.StoreId, opt => opt.MapFrom(model => model.Id))
.ForMember(entity => entity.Store, opt => opt.MapFrom(model => model))
.ForMember(entity => entity.ProductId, opt => opt.Ignore())
.ForMember(entity => entity.Product, opt => opt.Ignore());
CreateMap<ProductModel, ProductEntity>()
.ForMember(d => d.Stores,
opt => opt.MapFrom(s => s.Stores))
.AfterMap((model, entity) =>
{
foreach (var entityStore in entity.Stores)
{
entityStore.ProductId = entity.Id;
entityStore.Product = entity;
}
});
CreateMap<ProductModel, StoreProductEntity>()
.ForMember(entity => entity.StoreId, opt => opt.Ignore())
.ForMember(entity => entity.Store, opt => opt.Ignore())
.ForMember(entity => entity.ProductId, opt => opt.MapFrom(model => model.Id))
.ForMember(entity => entity.Product, opt => opt.MapFrom(model => model));
}
}
class Program
{
static void Main(string[] args)
{
var configuration = new MapperConfiguration(cfg =>
{
cfg.AddProfile<CustomProfile>();
});
#if DEBUG
configuration.AssertConfigurationIsValid();
#endif
var mapper = configuration.CreateMapper();
var store0 = new StoreModel()
{
Id = 1,
Name = "Store0",
};
var store1 = new StoreModel()
{
Id = 2,
Name = "Store1",
};
var product = new ProductModel()
{
Id = 1,
Name = "Product",
};
store1.Products.Add(product);
product.Stores.Add(store1);
store0.Products.Add(product);
product.Stores.Add(store0);
var store0Entity = mapper.Map<StoreEntity>(store0);
Debug.Assert(store0Entity.Products[0].Product.Stores[0].Store.Id ==
store0Entity.Products[0].Product.Stores[0].Store.Products[0].StoreId);
}
}
}
Mapping pass successfully, but by some reason some deep key is not mapped to the related StoreEntity
The following assertion is failed ...
Debug.Assert(store0Entity.Products[0].Product.Stores[0].Store.Id ==
store0Entity.Products[0].Product.Stores[0].Store.Products[0].StoreId);
Seems like by some reason in depth Store class AutoMapper uses wrong mapped List ... but I am not sure ...
I have figured out how to fixed this issue (thanks to answer on question Automapper creates 2 instances of one object)
For detecting Circular references I can add .PreserveReferences() method for mapping
Here is proper CustomProfile:
public class CustomProfile : Profile
{
public CustomProfile()
{
CreateMap<StoreModel, StoreEntity>()
.ForMember(d => d.Products,
opt => opt.MapFrom(s => s.Products))
.AfterMap((model, entity) =>
{
foreach (var entityProduct in entity.Products)
{
entityProduct.StoreId = entity.Id;
entityProduct.Store = entity;
}
})
.PreserveReferences();
CreateMap<StoreModel, StoreProductEntity>()
.ForMember(entity => entity.StoreId, opt => opt.MapFrom(model => model.Id))
.ForMember(entity => entity.Store, opt => opt.MapFrom(model => model))
.ForMember(entity => entity.ProductId, opt => opt.Ignore())
.ForMember(entity => entity.Product, opt => opt.Ignore());
CreateMap<ProductModel, ProductEntity>()
.ForMember(d => d.Stores,
opt => opt.MapFrom(s => s.Stores))
.AfterMap((model, entity) =>
{
foreach (var entityStore in entity.Stores)
{
entityStore.ProductId = entity.Id;
entityStore.Product = entity;
}
})
.PreserveReferences();
CreateMap<ProductModel, StoreProductEntity>()
.ForMember(entity => entity.StoreId, opt => opt.Ignore())
.ForMember(entity => entity.Store, opt => opt.Ignore())
.ForMember(entity => entity.ProductId, opt => opt.MapFrom(model => model.Id))
.ForMember(entity => entity.Product, opt => opt.MapFrom(model => model));
}
}
But actually I did not know if it is an issue or not, because according the documentation AutoMapper should detect Circular References automatically for AutoMapper version 10.0.0 and seems like this is an issue

Automapper Map multiple optional properties to list

I have a class OrderLineRequest that I want to map to an OrderLine class with a list of barcodes. The properties Barcode1,2,3 needs to be mapped to Barcodes only if the contain a value. Barcode1 is always filled, Barcode2 and Barcode3 are optional. I have created a mapping but this gives me always 3 barcodes in the list. If Barcode1 or 2 is an empty string i don't want to add them to the list. How can i do this?
public class OrderLineRequest
{
public string OrderLineId { get; set; }
public string Barcode1 { get; set; }
public string Barcode2 { get; set; }
public string Barcode3 { get; set; }
public int Quantity { get; set; }
}
public class OrderLine
{
public int Id { get;set;}
public int OrderId { get;set;}
public string OrderLineNumber { get; set; }
public int Qty { get; set; }
public List<Barcode> Barcodes { get;set;}
}
public class Barcode
{
public int Id { get;set;}
public int OrderLineId { get;set;}
public string Code { get;set;}
}
CreateMap<OrderLineRequest, OrderLine>()
.ForMember(b => b.Id, e => e.Ignore())
.ForMember(d => d.OrderId, p => p.Ignore())
.ForMember(d => d.OrderLineNumber, p => p.MapFrom(s => s.OrderLineId))
.ForMember(d => d.Qty, p => p.MapFrom(s => s.Quantity))
.ForMember(d => d.BarCodes, p => p.MapFrom(s => new List<EanCode>() { new EanCode(){Code = s.Barcode1}, new EanCode() { Code = s.Barcode2 }, new EanCode() { Code = s.Barcode3 } }));
Why are you always creating those three barcodes?
I would suggest you to create a function for the predicate
that accepts OrderLineRequest and returns your List and handle the creation within the function. Like that:
private List<EanCode> Foo(OrderLineRequest orderLineRequest)
{
var result = new List<EanCode>();
if(!string.IsNullOrEmpty(orderLineRequest.Barcode1)
result.Add(new EanCode {Code = orderLineRequest.Barcode1});
//...
return result;
}
And then you could use it like:
.ForMember(d => d.BarCodes, p => p.MapFrom(s => Foo(s)));
If you're using Automapper, the step of adding three specific properties from the source to a list in the destination can't be accomplished with a simple function. You have to tell Automapper how to accomplish it.
You can do that by telling it to ignore those properties during the initial mapping, and then add items to the destination list after that mapping is complete.
For brevity this includes only those properties:
var configuration = new MapperConfiguration(
cfg => cfg.CreateMap<OrderLineRequest, OrderLine>()
.ForMember(d => d.Barcodes, opt => opt.Ignore())
.ForSourceMember(s => s.Barcode1, opt => opt.DoNotValidate())
.ForSourceMember(s => s.Barcode2, opt => opt.DoNotValidate())
.ForSourceMember(s => s.Barcode3, opt => opt.DoNotValidate())
.AfterMap((source, destination) =>
{
destination.Barcodes = new List<Barcode>
{
new Barcode { Code = source.Barcode1 }
};
if (source.Barcode2 != null)
destination.Barcodes.Add(new Barcode { Code = source.Barcode2 });
if (source.Barcode3 != null)
destination.Barcodes.Add(new Barcode { Code = source.Barcode3 });
}));
It could be said that this makes a case for just writing your own extension instead of using Automapper. It's convenient when the mapping is simple, but if it's not then using it could arguably be more trouble than it's worth. That's a matter of preference.
Create an array of the three properties. Once it's in an array, you can use Where to remove the nulls and Select to instantiate the EanCode instances. Once the data are in good shape, call ToList().
.ForMember
(
d => d.BarCodes,
p => p.MapFrom
(
s =>
(new [] { s.BarCode1, s.BarCode2, s.BarCode3 })
.Where( x => x != null)
.Select( x => new EanCode { Code = x } )
.ToList()
)
);

automapper mapping property stays null

it seems like i cannot get nested mappings working properly
after reading: http://docs.automapper.org/en/stable/Nested-mappings.html it should be very easily.
i have the following classes:
public class CatalogVehicle : VehicleBase
{
public string Type { get; set; }
public int Year { get; set; }
public VehicleSpecification VehicleSpecification { get; set; } = new VehicleSpecification();
} //removed some properties for readability
public class VehicleSpecification
{
public Engine Engine { get; set; } = new Engine();
public Transmission Transmission { get; set; } = new Transmission();
public int Co2 { get; set; }
public int Weight { get; set; }
} //again removed some more properties (all classes)
for mapping:
CreateMap<VehicleAndQuote, CatalogVehicle>()
.ForMember(catalogVehicle => catalogVehicle.Id,
source => source.MapFrom(vehicleAndQuote => vehicleAndQuote.Quote.QuotationIdentifier.Trim()))
.ForMember(catalogVehicle => catalogVehicle.Make,
source => source.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.VehicleMakeName))
.ForMember(catalogVehicle => catalogVehicle.Model,
source => source.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.VehicleModelTypeName))
.ForMember(catalogVehicle => catalogVehicle.VehicleSpecification, opt => opt.Ignore()); //removed some lines
CreateMap<VehicleAndQuote, VehicleSpecification>()
.ForMember(vehicleSpecification => vehicleSpecification.Co2, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.Co2.ToSafeInt()))
.ForMember(vehicleSpecification => vehicleSpecification.Weight, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.Weight.ToSafeInt()))
.ForMember(vehicleSpecification => vehicleSpecification.Rating, opt => opt.Ignore())
.ForMember(vehicleSpecification => vehicleSpecification.Tyres, opt => opt.Ignore()) //removed some lines as well
CreateMap<VehicleAndQuote, Engine>()
.ForMember(engine => engine.Displacement, opt => opt.Ignore())
.ForMember(engine => engine.Fuel, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.FuelType))
.ForMember(engine => engine.Power, src => src.ResolveUsing(GetEnginePower))
.ForMember(engine => engine.Cylinders, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.Cylinders));
//etc
as you can see i am ignoring the some properties because otherwise i get the unmapped properties error. After reading the article it should work, as long as you have all classes mapped.
i am calling the method like: var vehicle = _mapper.Map<VehicleAndQuote, CatalogVehicle>(vehicleAndQuote); <= this is the big class that contains all the information
so from mapping from VehicleAndQuote to CatalogVehicle -first few properties work properly, but then the mapping to the VehicleSpecification lies my problem. that one will not be populated properly...
does anyone see the problem?
You need to configure mapping for nested classes instead of ignoring them:
.ForMember(catalogVehicle => catalogVehicle.VehicleSpecification, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote);
instead of
.ForMember(catalogVehicle => catalogVehicle.VehicleSpecification, opt => opt.Ignore());
And the same for other mappings

Can't configure ReverseMap in AutoMapper

I'm a AutoMapper newb. My mappings are not working as expected and I'm sure I'm doing somthing wrong but can't figure it out. Sorry if this question is confusing, but i'll do my best to be clear. Lets say we have three classes:
public class Person
{
public ContactInfo1 Contact { get; set; }
}
public class ContactInfo1
{
public string Name { get; set; }
}
public class ContactInfo2
{
public string AnotherName { get; set; }
}
Now, I want to setup my mappings so that ContactInfo1 can map to and from ContactInfo2. And then I want to be able to map Person1 -> ContactInfo2 (which might look stange, but I need to do it anyway). I have tried the following mapping config:
var autoMapperConfig = new AutoMapper.MapperConfiguration(cfg =>
{
cfg.CreateMap<ContactInfo1, ContactInfo2>()
.ForMember(dest => dest.AnotherName, opt => opt.MapFrom(src => src.Name)).ReverseMap();
cfg.CreateMap<ContactInfo2, Person>()
.ForMember(dest => dest.Contact, opt => opt.MapFrom(src => src)).ReverseMap();
});
var mapper = autoMapperConfig.CreateMapper();
For the test data:
var testPerson = new Person();
testPerson.Contact = new ContactInfo1() { Name = "Person1" };
I do the following:
var contactInfo2Test = mapper.Map<Person, ContactInfo2>(testPerson);
This does NOT give me any errors, but contactInfo2Test.AnotherName is empty. Please advise! Thanks.
Please note that I realize I could go:
cfg.CreateMap<Person, ContactInfo2>()
.ForMember(dest => dest.AnotherName, opt => opt.MapFrom(src => src.Contact.Name));
Bu then I would have mapped Contact1->Contact2 all over again, and in a more complex scenario I really want to avoid that.
Here's one way of doing it:
var autoMapperConfig = new AutoMapper.MapperConfiguration(cfg =>
{
cfg.CreateMap<ContactInfo1, ContactInfo2>()
.ForMember(dest => dest.AnotherName, opt => opt.MapFrom(src => src.Name))
.ReverseMap();
cfg.CreateMap<Person, ContactInfo2>()
.ConstructUsing((p, ctx) => ctx.Mapper.Map<ContactInfo2>(p.Contact));
});

Automapper - How to map to IEnumerable

I'm making Forum system, I have SubCategoryThreadsViewModel in which I'm trying to map LastComment and Date of last post for every thread. This is my code:
public class SubCategoryThreadsViewModel : IHaveCustomMappings
{
public string Title { get; set; }
public string Description { get; set; }
public IEnumerable<Thread> Threads { get; set; }
public ThreadInfoSubCategoryViewModel ThreadInfoSubCategoryViewModel { get; set; }
public void CreateMappings(IConfiguration configuration)
{
configuration.CreateMap<Thread, SubCategoryThreadsViewModel>()
.ForMember(m => m.Title, opt => opt.MapFrom(t => t.SubCategory.Title))
.ForMember(m => m.Description, opt => opt.MapFrom(t => t.SubCategory.Description))
.ForMember(m => m.Threads, opt => opt.MapFrom(t => t.SubCategory.Threads))
.ForMember(m => m.ThreadInfoSubCategoryViewModel, opt => opt.MapFrom(t => new ThreadInfoSubCategoryViewModel()
{
LastCommentBy = t.Posts.Select(a => a.Author.UserName),
DateOfLastPost = t.Posts.Select(a => a.CreatedOn.ToString()),
}))
.ReverseMap();
}
The code
.ForMember(m => m.ThreadInfoSubCategoryViewModel, opt => opt.MapFrom(t => new ThreadInfoSubCategoryViewModel()
{
LastCommentBy = t.Posts.Select(a => a.Author.UserName),
DateOfLastPost = t.Posts.Select(a => a.CreatedOn.ToString()),
}))
is working but only when property ThreadInfoSubCategoryViewModel is not Ienumerable as in the code above, and inside are two IEnumerable strings.
public class ThreadInfoSubCategoryViewModel
{
public IEnumerable<string> LastCommentBy { get; set; }
public IEnumerable<string> DateOfLastPost { get; set; }
}
This works, but I want ThreadInfoSubCategoryViewModel to be Ienumerable, and in the class properties to be string for easy foreach.
I have tried to make it IEnumerable, but with current automapper code it doesn't work.
You need to manually map the member to an IEnumerable<ThreadInfoSubCategoryViewModel> rather than a single object.
I assume each Post in t.Posts represents one ThreadInfoSubCategoryViewModel, so a simple Select() should do it:
public IEnumerable<ThreadInfoSubCategoryViewModel> ThreadInfoSubCategoryViewModel { get; set; }
...
.ForMember(m => m.ThreadInfoSubCategoryViewModel, opt => opt.MapFrom(t =>
t.Posts.Select(p => new ThreadInfoSubCategoryViewModel()
{
LastCommentBy = p.Author.UserName,
DateOfLastPost = p.CreatedOn.ToString()
})
))

Categories