Unflattening data list into a nested list using AutoMapper - c#

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

Related

How select hierarchy from child to parent using linq for entities or EF Core 2?

There is a hierarchy of four entities (models). All entities have relationships one to many. It's looks following:
public class Region
{
public Guid Id { get; set; }
public int RegionId { get; set; }
public virtual ICollection<District> Districts { get; set; }
}
public class District
{
public Guid Id { get; set; }
public int DistrictId { get; set; }
public virtual ICollection<Building> Buildings { get; set; }
public int RegionId { get; set; }
public Region Region { get; set; }
}
public class Building
{
public Guid Id { get; set; }
public int BuildingId { get; set; }
public int DistrictId { get; set; }
public virtual ICollection<Flat> Flats { get; set; }
public District District { get; set; }
}
public class Flat
{
public Guid Id { get; set; }
public int FlatId { get; set; }
public int BuildingId { get; set; }
public Building Building { get; set; }
}
It is hierarchy like: Region->District->Building->Flat.
I want return collection of Flat thats include all related entities from top.
I do it folowing:
var flat = _context.Flats
.Include(t => t.Building)
.Include(b=>b.Building.District)
.Include(b => b.Building.District.Region).GetPaged(PageNumber, 20);
But for some reason is return breaking result....
[{"id":"6cbb1431-36d4-490c-a047-c211e4db3bdd","flatId":29570,"roomsCount":1,"totalArea":41.95,"kitchenArea":11.85,"floor":21,"buildingId":130,"price":5725650.00,"building":{"id":"03baca1e-02e9-4ee0-ae47-920e3eb0d5bb","buildingId":130,"name":"NNN","queue":1,"housing":"1","districtId":13003,"flats":[
It breaks for some reason on public virtual ICollection<Flat> Flats { get; set; }. I can't remove it becasue it is needed for FK.
The following query works:
_context.Flat
.Join(
_context.Building,
f => f.BuildingId,
b => b.BuildingId,
(f, b) =>
new
{
f = f,
b = b
}
)
.Join(
_context.District,
temp0 => temp0.b.DistrictId,
d => d.DistrictId,
(temp0, d) =>
new
{
temp0 = temp0,
d = d
}
)
.Join(
_context.Region,
temp1 => temp1.d.RegionId,
r => r.RegionId,
(temp1, r) =>
new
{
Id = temp1.temp0.f.Id,
FlatId = temp1.temp0.f.FlatId,
Floor = temp1.temp0.f.Floor,
RoomsCount = temp1.temp0.f.RoomsCount,
TotalArea = temp1.temp0.f.TotalArea,
KitchenArea = temp1.temp0.f.KitchenArea,
Price = temp1.temp0.f.Price,
Building = new
{
Id = temp1.temp0.b.Id,
BuildingId = temp1.temp0.b.Id,
Name = temp1.temp0.b.Name,
Queue = temp1.temp0.b.Queue,
Housing = temp1.temp0.b.Housing,
District = new
{
Id = temp1.d.Id,
DistrictId = temp1.d.DistrictId,
Name = temp1.d.Name,
Regions = new
{
Id = r.Id,
RegionId = r.Id,
Name = r.Name
}
}
}
}
).GetPaged(PageNumber, PageSize)
And it is return what i whant:
{
"id": "a49a0a48-370b-46da-b6da-ea4040cc1431",
"flatId": 84095,
"roomsCount": 3,
"totalArea": 76.5,
"kitchenArea": 11.07,
"floor": 18,
"buildingId": 0,
"price": 5567500,
"building": {
"id": "037c4074-3ff5-4fbd-92ea-88c8f746b883",
"buildingId": 200,
"name": "NNNN",
"queue": 3,
"housing": "4",
"districtId": 0,
"flats": null,
"district": {
"id": "db031e09-1d6c-41f4-9e45-ec6170e3df8b",
"districtId": 5005,
"name": "Distr",
"buildings": null,
"regionId": 0,
"region": {
"id": "5ac5766f-e62e-4da9-86cf-e4a85423503a",
"regionId": 5001,
"name": "Region",
"districts": null
}
}
}
}
But it is looks like cumbersome.
How can I do it using EF features? Or maybe somehow to reduce linq? And why include() return breaking result?
P.S. .GetPaged(PageNumber, 20); is extension method for pagination and he is working fine.
My suggesstion is below
var flag = (from f in _context.Flags
join b in _context.Buildings
on b.BuildingId equals f.BuildingId
join d in _context.Districts
on d.DistrictId equals b.DistrictId
join r in _context.Regions
on r.RegionId equals d.RegionId
select new
{ ...
}).GetPaged(PageNumber, 20);
You can get any property from Building, District and Region to populate to your result object.

How to map objects

Here is my how my bindings currently look:
MerchantAccountRequest request = new MerchantAccountRequest
{
Individual = new IndividualRequest
{
FirstName = merchant.MerchantIndividual.FirstName,
LastName = merchant.MerchantIndividual.LastName,
Email = merchant.MerchantIndividual.Email,
Phone = merchant.MerchantIndividual.Phone,
DateOfBirth = merchant.MerchantIndividual.DateOfBirth,
Ssn = merchant.MerchantIndividual.Ssn,
Address = new AddressRequest
{
StreetAddress = merchant.MerchantIndividual.StreetAddress,
Locality = merchant.MerchantIndividual.Locality,
Region = merchant.MerchantIndividual.Region,
PostalCode = merchant.MerchantIndividual.PostalCode
}
},
Business = new BusinessRequest
{
LegalName = merchant.MerchantBusiness.LegalName,
DbaName = merchant.MerchantBusiness.DbaName,
TaxId = merchant.MerchantBusiness.TaxId,
Address = new AddressRequest
{
StreetAddress = merchant.MerchantBusiness.StreetAddress,
Locality = merchant.MerchantBusiness.Locality,
Region = merchant.MerchantBusiness.Region,
PostalCode = merchant.MerchantBusiness.PostalCode
}
},
Funding = new FundingRequest
{
Descriptor = merchant.MerchantFunding.Descriptor,
Destination = FundingDestination.BANK,
Email = merchant.MerchantFunding.Email,
MobilePhone = merchant.MerchantFunding.MobilePhone,
AccountNumber = merchant.MerchantFunding.AccountNumber,
RoutingNumber = merchant.MerchantFunding.RoutingNumber
},
TosAccepted = merchant.TosAccepted,
MasterMerchantAccountId = merchant.MasterMerchantAccountId,
Id = merchant.MerchantId
};
And I need to use AutoMapper to achieve the above.
This is what I've tried:
CreateMapperProfile
EDIT:
CreateMap<Classes.Merchant, MerchantAccountRequest>()
.ForMember(dest => dest.Individual, source => source.MapFrom(s => s.MerchantIndividual))
.ForMember(dest => dest.Business, source => source.MapFrom(s => s.MerchantBusiness))
.ForMember(dest => dest.Funding, source => source.MapFrom(s => s.MerchantFunding))
.ForMember(dest => dest.Id, source => source.MapFrom(s => s.MerchantId));
Here is the Merchant class:
public partial class Merchant :
{
public int Id { get; set; }
public string MerchantId { get; set; }
public virtual MerchantIndividual MerchantIndividual { get; set; }
public virtual MerchantBusiness MerchantBusiness { get; set; }
public virtual MerchantFunding MerchantFunding { get; set; }
public bool TosAccepted { get; set; }
public string MasterMerchantAccountId { get; set; }
public bool isSubMerchant { get; set; }
}
And the mappings;
EDIT:
MerchantAccountRequest request = _mapper.Map<MerchantAccountRequest>(merchant);
request.Individual = _mapper.Map<IndividualRequest>(merchant.MerchantIndividual);
request.Business = _mapper.Map<BusinessRequest>(merchant.MerchantBusiness);
request.Funding = _mapper.Map<FundingRequest>(merchant.MerchantFunding);
Can the first line of code MerchantAccountRequest request = _mapper.Map<MerchantAccountRequest>(merchant); above do all the mapings?
..
..
..
..
How can I create the correct mappings?
You don't need to call _mapper.Map on the properties of the request.
Just call MerchantAccountRequest request = _mapper.Map<MerchantAccountRequest>(merchant); and assuming you have a map for each type you should be fine.
I think something along the lines of the following should get you going down the right path.
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Merchant, MerchantAccountRequest>()
.ForMember(dest => dest.Individual, c => c.MapFrom(source => source.MerchantIndividual));
cfg.CreateMap<MerchantIndividual, IndividualRequest>();
});
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
var merchant = new Merchant
{
Id = 1,
MerchantIndividual = new MerchantIndividual { FirstName = "John Doe" }
};
var merchantAccountRequest = mapper.Map<Merchant, MerchantAccountRequest>(merchant);
}
}
public class Merchant
{
public int Id { get; set; }
public MerchantIndividual MerchantIndividual { get; set; }
}
public class MerchantIndividual
{
public string FirstName { get; set; }
}
public class MerchantAccountRequest
{
public int Id { get; set; }
public IndividualRequest Individual { get; set; }
}
public class IndividualRequest
{
public string FirstName { get; set; }
}

How to use ForMember properly inside a collection?

I have my source class as
public class SourceEmployee
{
public int EmployeeID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<ResidentialAddress> EmployeeResidences { get; set; }
}
and the ResidentialAddress is as under
public class ResidentialAddress
{
public string State { get; set; }
public string City { get; set; }
public int ZipCode { get; set; }
}
The Destination class is as under
public class DestinationEmployee
{
public int EmployeeID { get; set; }
public string FullName { get; set; }
public List<ResidentialAddress1> Address { get; set; }
}
public class ResidentialAddress1
{
public string FullAddress { get; set; }
}
How to perform the ForMember for the FullAddress which will be State + City + ZipCode ?
I am lost after
Mapper.CreateMap<SourceEmployee, DestinationEmployee>();
Mapper.CreateMap<SourceEmployee, DestinationEmployee>().
ForMember(f => f.FullName, f => f.MapFrom(a => string.Concat(a.FirstName, " ", a.LastName)))
.ForMember(x => x.EmployeeResidences1, x => x.MapFrom(y => string.Concat(y.EmployeeResidences.m, " ", y.LastName)));
Well, you can use LINQ to let AutoMapper know how to map 3 properties into one property and you shouldn't really use Mapper.CreateMap() as it is deprecated and will not be supported from version 5.0 - use Mapper.Initialize() instead.
Let's have a look at this example :
Mapper.Initialize(cfg =>
{
cfg.CreateMap<SourceEmployee, DestinationEmployee>();
cfg.CreateMap<SourceEmployee, DestinationEmployee>()
.ForMember(f => f.FullName, f => f.MapFrom(a => string.Concat(a.FirstName, " ", a.LastName)))
.ForMember(
x => x.Address,
x => x.MapFrom(
y => y.EmployeeResidences.Select(
r => new ResidentialAddress1()
{
FullAddress = String.Concat(
r.State, " ", r.City, " ", r.ZipCode)
}).ToList()));
});
SourceEmployee emp = new SourceEmployee()
{
EmployeeID = 1,
FirstName = "Alex",
LastName = "Green",
EmployeeResidences = new List<ResidentialAddress>()
{
new ResidentialAddress() { State = "abc", City = "def", ZipCode = 110 },
new ResidentialAddress() { State = "foo", City = "qwe", ZipCode = 220 },
new ResidentialAddress() { State = "bar", City = "ert", ZipCode = 330 },
}
};
var sourceEmp = Mapper.Map<SourceEmployee, DestinationEmployee>(emp);
Console.WriteLine(sourceEmp.Address.Count);
Console.WriteLine(sourceEmp.Address[1].FullAddress);
Output :
3
foo qwe 220
You should have a separate map profile for your ResidentialAddress -> ResidentialAddress1 transformation. When your parent object will be transformed using automapper, all child objects will get transformed using defined map profiles:
Mapper.CreateMap<SourceEmployee, DestinationEmployee>()
.ForMember(f => f.FullName, f => f.MapFrom(a => string.Concat(a.FirstName, " ", a.LastName)))
.ForMember(x => x.Address, x => x.MapFrom(y => y.EmployeeResidences)));
Mapper.CreateMap<ResidentialAddress, ResidentialAddress1>
.ForMember(x => x.FullAddress, map=>map.From(from => string.Format("{0} {1} {2}", from.State, from.City, from.ZipCode);
This way if in code you will have to transform ResidentialAddress to ResidentialAddress1 in multiple places, you won't have to add any code, just use Mapper.Map<>.
I also suggest you to switch to profiles, instead of defining your mapper configurations inline: https://github.com/AutoMapper/AutoMapper/wiki/Configuration

How can I create set contain id record in first set equal id record in second set?

In database I have two tables:
public partial class PersonOne
{
public int id { get; set; }
public string name { get; set; }
public string surname { get; set; }
}
public partial class PersonTwo
{
public int id { get; set; }
public string firstname { get; set; }
public string lastname { get; set; }
}
I would like to fill my set:
public class PersonOnePersonTwo
{
public int PersonOneId { get; set; }
public int PersonTwoId { get; set; }
}
where PersonOne.name == PersonTwo.firstname && PersonOne.surname == PersonTwo.lastname but I have no idea how I can do that - because below code isn't efficient and is so slow:
List<PersonOne> personOneList = new List<PersonOne>();
List<PersonTwo> personTwoList = new List<PersonTwo>();
List<PersonOnePersonTwo> personOnePersonTwoList = new List<PersonOnePersonTwo>();
foreach (PersonOne personOne in personOneList)
{
foreach(PersonTwo personTwo in personTwoList.Where(x => x.firstname == personOne.name && x.lastname == personOne.surname).ToList())
{
personOnePersonTwoList.Add(new PersonOnePersonTwo
{
PersonOneId = personOne.id,
PersonTwoId = personTwo.id
});
}
};
Try this:
var result = personOneList.Join
(
personTwoList,
person1 => new { Key1 = person1.Name, Key2 = person1.Surname },
person2 => new { Key1 = person2.FirstName, Key2 = person2.LastName },
(person1, person2) => new PersonOnePersonTwo { PersonOneId = person1.Id, PersonTwoId = person2.Id }
).ToList();
I would go with:
var personOnePersonTwoList = new List<PersonOnePersonTwo>();
foreach (var personOne in personOneList)
{
personOnePersonTwoList = personTwoList.Where(x => x.firstname.Equals(personOne.name, StringComparison.OrdinalIgnoreCase) &&
x.lastname.Equals(personOne.surname, StringComparison.OrdinalIgnoreCase))
.Select(x => new PersonOnePersonTwo {PersonOneId = personOne.id, PersonTwoId = x.id}).ToList();
};
As a side note: it's more convinient to use Equals when comparing strings.

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

Categories

Resources