Flatten Nested List using AutoMapper - c#

I'm trying to flatten a nested object into a DTO object in .NET 3.5. Most of what I've seen so far is to use AutoMapper to do this (using v1.x since I need to use .NET 3.5, unfortunately):
Here's what a snippet of my class structures look like:
public class RootObject
{
[JsonProperty("BaseSupplier")]
public BaseSupplier BaseSupplier { get; set; }
[JsonProperty("BaseOrderShipmentLineitem")]
public IList<BaseOrderShipmentLineitem> BaseOrderShipmentLineitem { get; set; }
}
public class BaseSupplier
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
public class BaseOrderShipmentLineitem
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("qty_delivered")]
public int QtyDelivered { get; set; }
[JsonProperty("BaseOrderLineitem")]
public BaseOrderLineitem BaseOrderLineitem { get; set; }
}
public class BaseOrderLineitem
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("product_sku")]
public string ProductSku { get; set; }
}
public class ShipmentDetailsDTO
{
public int BaseOrderShipmentLineitemId { get; set; }
public string BaseSupplierName { get; set; }
public string Sku { get; set; }
}
I've been trying something like this:
Mapper.CreateMap<BaseOrderLineitem, ShipmentDetailsDTO>()
.ForMember(d => d.Sku, opts => opts.MapFrom(s => s.ProductSku));
Mapper.CreateMap<BaseOrderShipmentLineitem, ShipmentDetailsDTO>();
Mapper.CreateMap<RootObject, ShipmentDetailsDTO>()
.ForMember(d => d.Sku, opts => opts.MapFrom(s => Mapper.Map<IEnumerable<BaseOrderLineitem>, IEnumerable<ShipmentDetailsDTO>>(s.BaseOrderShipmentLineitem.SelectMany(q => q.BaseOrderLineitem)).FirstOrDefault().Sku))
;
var model = Mapper.Map<IEnumerable<RootObject>, IEnumerable<ShipmentDetailsDTO>>(obj);
With that above code I'm getting an error on this bit s.BaseOrderShipmentLineitem.SelectMany(q => q.BaseOrderLineitem):
Cannot implicitly convert type 'IEnumerable<?>' to 'IEnumerable<BaseOrderLineitem>'. An explicit conversion exists (are you missing a cast?)
I'm not sure if it's something simple I'm just overlooking or not.

A far easier way is to write a simple extension method using some LINQ to do this projection yourself. It's easier and more transparent:
public static class MyConversionExtensions
{
public static IEnumerable<ShipmentDetailsDTO> ToShipmentDetails(this RootObject root)
{
return root.BaseOrderShipmentLineitem.Select(x => new ShipmentDetailsDTO() {
BaseOrderShipmentLineitemId = x.BaseOrderLineitem.Id,
BaseSupplierName = root.BaseSupplier.Name,
Sku = x.BaseOrderLineitem.ProductSku
});
}
}
Usage:
var shipmentDetails = myRootObject.ToShipmentDetails();

Related

How can i fix Cannot implicitly convert Generic.List< > to Generic.List< >

How can i fix Cannot implicitly convert System.Collections.Generic.List <RPHistory> to System.Collections.Generic.List <RPHistory> exception error.
I am trying to combine two Entities together to get a single list
RP Entity Class:
public class RP
{
public int ID { get; set; }
public int RPID { get; set; }
public string Name { get; set; }
public int ProductID { get; set; }
}
RPHistory Entity Class :
public class RPHistory:
{
public int ID { get; set; }
public int RPID { get; set; }
public string Name { get; set; }
public int ProductID { get; set; }
}
And I created this third class
RpWithHistory Class :
public class RpWithHistory {
public int ID;
public int RPID;
public string Name;
public int ProductID;
public List<RPHistory> History;
}
Linq Query
var RPs = await Context.RP.Where(b => b.ProductID == request.ID)
.Select(x=> new RpWithHistory {
ID = x.ID,
RPID = x.RPID,
Name = x.Name,
ProductID = x.ProductID,
History = Context.RPHistory
.Where(y=> y.RPID
== x.RPID)
.ToList()
}
).ToListAsync();
But i get this error,
>Cannot implicitly convert System.Collections.Generic.List <RPHistory> to
>System.Collections.Generic.List <RPHistory> exception error
Thanks!
I am not sure why you're doing that. Can I suggest this?
You do not need to go all that way of creating a class that joins the two. Just create a Navigation property on your RP that points to RPHistory Objects.
public class RP
{
public int ID { get; set; }
public int RPID { get; set; }
public string Name { get; set; }
public int ProductID { get; set; }
public ICollection<RPHistory> HistoryList { get; set; } // Navigation Property
}
public class RPHistory:
{
public int ID { get; set; }
public int RPID { get; set; }
public string Name { get; set; }
public int ProductID { get; set; }
[ForeignKey(nameof(RPID))] // Identify the Foreign Key from RP Class
public RP RP { get; set; } // Navigation back to RP
}
Then you can chain everything into a single list using LINQ:
var RPs = Context.RP.Where(rp => rp.ProductID == request.ID)
.Include(rp=>rp.RPHistory) // This includes RPHistory
.ToList();
You need to clone or create a new list.
Option 1: Use ConvertAll
List<RPHistory> pPHistoryCopy = rphWithHistory.RPHistory.ConvertAll(history => new RPHistory(rphWithHistory.RPHistory));
Option 2:
//Clone Extension
static class Extensions
{
public static IList<T> Clone<T>(this IList<T> listToClone) where T: ICloneable
{
return listToClone.Select(item => (T)item.Clone()).ToList();
}
}
Use the clone extention

Load navigation properties in generic repository

I have a problem when using AutoMapper and EF Core together to map navigation properties from the model to the DTO. My EF classes are:
public class Meal
{
public DateTime Day { get; set; }
public MealType MealType { get; set; }
public int MealId { get; set; }
}
public class MealType
{
public string Name { get; set; }
public int MealTypeId { get; set; }
}
And the corresponding DTO classes:
public class ExistingMealDto
{
public DateTime Day { get; set; }
public ExistingMealTypeDto MealType { get; set; }
public int MealId { get; set; }
public string MealTypeName { get; set; }
}
public class ExistingMealTypeDto
{
public string Name { get; set; }
public int MealTypeId { get; set; }
}
This is my AutoMapper mapping:
config.CreateMap<DataLayer.EfClasses.MealType, ExistingMealTypeDto>();
config.CreateMap<DataLayer.EfClasses.Meal, ExistingMealDto>()
.ForMember(x => x.MealType, x => x.MapFrom(x=>x.MealType))
.ForMember(x => x.MealTypeName, x => x.MapFrom(y => y.MealType.Name));
I'm loading the data within a generic method that looks like this:
public IEnumerable<TDtoOut> GetAllAsDto<TIn, TDtoOut>()
where TIn : class
{
var allEntities = DbContext.Set<TIn>();
return Mapper.Map<IEnumerable<TDtoOut>>(allEntities);
}
When calling this code, all Meal instances are loaded from the database and MealId and Day are filled correctly. However, MealType is null and therefore ExistingMealDto.MealType is null as well. I can work around this problem by explicitly calling DbContext.MealTypes.ToList(), but since the method should be generic for TIn, this is not a production solution.
How can I solve this issue? Thanks!
For getting the related data in generic method , you can judge the Type of the passed type. The following is a test demo , you could refer to:
public IEnumerable<TIn> GetAllAsDto<TIn>()
where TIn : class
{
Type typeParameterType = typeof(TIn);
if (typeParameterType == typeof(User))
{
var Entities = _context.Set<User>().Include(u=>u.Orders);
return (IEnumerable<TIn>)Entities;
}
else
{
var allEntities = _context.Set<TIn>();
return allEntities;
}
}
public void Test()
{
var data = GetAllAsDto<User>();
var data1 = GetAllAsDto<Status>();
}
Result

Automapper Sub Property Mapping

I have a situation where I need to map a sub-collection of items within an object to a collection of items in another object. I am essentially trying to flatten the object for use by a consuming system.
Given the following entity classes:
public class PersonEntity
{
public int Id { get; set; }
public virtual ICollection<OutcomeEntity> Outcomes { get; set; }
}
public class OutcomeEntity
{
public int Id { get; set; }
public bool Outcome { get; set; }
public virtual ICollection<GradeEntity> Grades { get; set; }
public PersonEntity Person { get; set; }
}
public class GradeEntity
{
public int Id { get; set; }
public string Grade { get; set; }
public string MarkersComment { get; set; }
public OutcomeEntity Outcome { get; set; }
}
I need to map the OutcomeEntity and GradeEntity to the following flattened structure where there can be many outcomes, containing many different grades:
public class PersonDTO
{
public int PersonId { get; set; }
public virtual ICollection<GradeDTO> Grades { get; set; }
}
public class GradeDTO
{
public int OutcomeId { get; set; }
public int GradeId { get; set; }
public string Grade { get; set; }
public string MarkersComment { get; set; }
}
Basically, for every Outcome in the collection, I want to iterate over the grades within it and create a new object (GradeDTO).
I have attempted to create a basic map, but I simply cannot get my head around the sub-properties.
To create one collection from many you can use SelectMany extension method. With this method and the following configuration AutoMapper will create PersonDto from PersonEntity.
Mapper.Initialize(cfg =>
{
cfg.CreateMap<GradeEntity, GradeDTO>()
.ForMember(dto => dto.GradeId, x => x.MapFrom(g => g.Id))
.ForMember(dto => dto.OutcomeId, x => x.MapFrom(g => g.Outcome.Id));
cfg.CreateMap<PersonEntity, PersonDTO>()
.ForMember(dto => dto.PersonId, x => x.MapFrom(p => p.Id))
.ForMember(dto => dto.Grades, x => x.MapFrom(p => p.Outcomes.SelectMany(o => o.Grades)));
});

AutoMapper with different children

I have an entity as Plan with multiple sub-plans (children), each of which could be null.
For the PlanDto, I am trying to load up a list of all children rather than having a separate property for each child like the entity.
I have already achieved it manually through a foreach loop but now I am trying to do it via AutoMapper, which is failing for some reason.
Entities:
public class Plan
{
public virtual int Id { get; set; }
public DateTime Date { get; set; }
public virtual PlanDetail PlanChild1 { get; set; }
public virtual ObservationCare PlanChild2 { get; set; }
}
public class PlanDetail
{
public virtual int Id { get; set; }
public virtual Plan Plan { get; set; }
public virtual string Description { get; set; }
}
public class ObservationCare
{
public virtual int Id { get; set; }
public virtual Plan Plan { get; set; }
public virtual string Description { get; set; }
}
DTOs:
public class PlanDto: EntityDto
{
public DateTime Date { get; set; }
public IEnumerable<ChildPlan> ChildPlan { get; set; }
}
public class ChildPlan : EntityDto
{
public ChildPlanType Type { get; set; }
}
public enum ChildPlanType
{
PlanDetail,
ObservationCare
}
AutoMapper config:
configuration.CreateMap<Plan, PlanDto>();
configuration.CreateMap<PlanDetail, ChildPlan>()
.ForMember(dto => dto.Type, options => options.MapFrom(p => ChildPlanType.PlanDetail));
configuration.CreateMap<ObservationCare, ChildPlan>()
.ForMember(dto => dto.Type, options => options.MapFrom(p => ChildPlanType.ObservationCare));
Mapping attempt:
var output = new List<PlanDto>();
var plans = await _planRepository.GetAll().ToList();
foreach (var plan in plans)
{
output.Add(ObjectMapper.Map<PlanDto>(plan));
}
I do not know why ChildPlan DTOs in the output list are always null!
You have to specify the mapping for PlanDto.ChildPlan:
configuration.CreateMap<Plan, PlanDto>()
.ForMember(dto => dto.ChildPlan,
options => options.MapFrom(
p => new object[] { p.PlanChild1, p.PlanChild2 }.Where(c => c != null)));
If you are using Entity Framework Core, you have to use eager-loading:
var plans = await _planRepository.GetAll()
.Include(p => p.PlanChild1)
.Include(p => p.PlanChild2)
.ToList();
There's also a simpler and more efficient way to map a list:
var output = ObjectMapper.Map<List<PlanDto>>(plans);

Automapper configure 2 classes to one

I have the following DB (Infrastructure) classes:
[Table("ApplicationDriverEquipments")]
public partial class ApplicationDriverEquipment
{
public int Id { get; set; }
[StringLength(256)]
public string Make { get; set; }
[StringLength(256)]
public string Model { get; set; }
[StringLength(256)]
public string Year { get; set; }
[StringLength(256)]
public string VINNumber { get; set; }
[StringLength(256)]
public string PlateNumber { get; set; }
[StringLength(256)]
public string CurrentMileage { get; set; }
[StringLength(256)]
public string Length { get; set; }
public int TypeId { get; set; }
public virtual ApplicationDriverEquipmentType Type { get; set; }
public int DriverId { get; set; }
public virtual ApplicationDriver Driver { get; set; }
}
[Table("ApplicationDriverEquipmentTypes")]
public partial class ApplicationDriverEquipmentType
{
public ApplicationDriverEquipmentType()
{
Equipments = new HashSet<ApplicationDriverEquipment>();
}
public int Id { get; set; }
[Required]
[StringLength(256)]
public string Name { get; set; }
public virtual ICollection<ApplicationDriverEquipment> Equipments { get; set; }
}
and the following DTO (Domain) classes:
public abstract class ApplicationDriverEquipmentAbstractDomain
{
public int Id { get; set; }
public string Make { get; set; }
public string Model { get; set; }
public string Year { get; set; }
public string PlateNumber { get; set; }
public string CurrentMileage { get; set; }
public string Type { get; protected set; }
}
public class ApplicationDriverEquipmentTractorDomain : ApplicationDriverEquipmentAbstractDomain
{
public ApplicationDriverEquipmentTractorDomain()
{
Type = ApplicationDriverEquipmentTypeStaticStringsDomain.Tractor;
}
public string VINNumber { get; set; }
}
public class ApplicationDriverEquipmentTrailerDomain : ApplicationDriverEquipmentAbstractDomain
{
public ApplicationDriverEquipmentTrailerDomain()
{
Type = ApplicationDriverEquipmentTypeStaticStringsDomain.Trailer;
}
public string Length { get; set; }
}
public class ApplicationDriverEquipmentStraightTruckDomain : ApplicationDriverEquipmentAbstractDomain
{
public ApplicationDriverEquipmentStraightTruckDomain()
{
Type = ApplicationDriverEquipmentTypeStaticStringsDomain.StraightTruck;
}
public string VINNumber { get; set; }
public string Length { get; set; }
}
public class ApplicationDriverEquipmentCargoVanDomain : ApplicationDriverEquipmentAbstractDomain
{
public ApplicationDriverEquipmentCargoVanDomain()
{
Type = ApplicationDriverEquipmentTypeStaticStringsDomain.CargoVan;
}
public string VINNumber { get; set; }
public string Length { get; set; }
}
public static class ApplicationDriverEquipmentTypeStaticStringsDomain
{
public const string Tractor = "Tractor";
public const string Trailer = "Trailer";
public const string StraightTruck = "Straight Truck";
public const string CargoVan = "Cargo Van";
}
I wrote the following Automapper rules to resolve it:
CreateMap<Domain.POCO.Application.ApplicationDriverEquipmentTractorDomain, Infrastructure.Asset.ApplicationDriverEquipment>()
.ForMember(c => c.Type.Name, p => p.UseValue<string>(Domain.StaticStrings.ApplicationDriverEquipmentTypeStaticStringsDomain.Tractor));
CreateMap<Domain.POCO.Application.ApplicationDriverEquipmentTrailerDomain, Infrastructure.Asset.ApplicationDriverEquipment>()
.ForMember(c => c.Type.Name, p => p.UseValue<string>(Domain.StaticStrings.ApplicationDriverEquipmentTypeStaticStringsDomain.Trailer));
CreateMap<Domain.POCO.Application.ApplicationDriverEquipmentStraightTruckDomain, Infrastructure.Asset.ApplicationDriverEquipment>()
.ForMember(c => c.Type.Name, p => p.UseValue<string>(Domain.StaticStrings.ApplicationDriverEquipmentTypeStaticStringsDomain.StraightTruck));
CreateMap<Domain.POCO.Application.ApplicationDriverEquipmentCargoVanDomain, Infrastructure.Asset.ApplicationDriverEquipment>()
.ForMember(c => c.Type.Name, p => p.UseValue<string>(Domain.StaticStrings.ApplicationDriverEquipmentTypeStaticStringsDomain.CargoVan));
I got an error:
Expression 'c => c.Type.Name' must resolve to top-level member and not
any child object's properties. Use a custom resolver on the child type
or the AfterMap option instead.
UPDATE
I rewrote maps:
CreateMap<Domain.POCO.Application.ApplicationDriverEquipmentTractorDomain, Infrastructure.Asset.ApplicationDriverEquipment>()
.AfterMap((src, dest)=> dest.Type.Name = Domain.StaticStrings.ApplicationDriverEquipmentTypeStaticStringsDomain.Tractor);
CreateMap<Domain.POCO.Application.ApplicationDriverEquipmentTrailerDomain, Infrastructure.Asset.ApplicationDriverEquipment>()
.AfterMap((src, dest) => dest.Type.Name = Domain.StaticStrings.ApplicationDriverEquipmentTypeStaticStringsDomain.Trailer);
CreateMap<Domain.POCO.Application.ApplicationDriverEquipmentStraightTruckDomain, Infrastructure.Asset.ApplicationDriverEquipment>()
.AfterMap((src, dest) => dest.Type.Name = Domain.StaticStrings.ApplicationDriverEquipmentTypeStaticStringsDomain.StraightTruck);
CreateMap<Domain.POCO.Application.ApplicationDriverEquipmentCargoVanDomain, Infrastructure.Asset.ApplicationDriverEquipment>()
.AfterMap((src, dest) => dest.Type.Name = Domain.StaticStrings.ApplicationDriverEquipmentTypeStaticStringsDomain.CargoVan);
but now I got an error:
Type Map configuration: ApplicationDriverEquipmentTractorDomain ->
ApplicationDriverEquipment
Domain.POCO.Application.ApplicationDriverEquipmentTractorDomain ->
Infrastructure.Asset.ApplicationDriverEquipment
Property: Type ---> AutoMapper.AutoMapperMappingException: Missing
type map configuration or unsupported mapping.
Mapping types:
String -> ApplicationDriverEquipmentType
System.String -> Infrastructure.Asset.ApplicationDriverEquipmentType
Seems, I don't understand how to map it correctly
You are trying to map from
ApplicationDriverEquipmentTractorDomain.Type is a string
to
ApplicationDriverEquipment.Type is a ApplicationDriverEquipmentType
Where is your mapping configuration for that?
Is it even possible to map a string to a ApplicationDriverEquipmentType?
Sure, you can have a string Name, but where do you get the Id and Equipments?
I suspect you don't want to create a new instance of that type each time you map, but rather you need to look up an instance from some dictionary, sort of a registry pattern
To implement this idea, you simply need to
Load all of the ApplicationDriverEquipmentType from DB
Put them in a dictionary (assuming name is unique)
Register a custom type converter or custom value resolver as below
One way to implement this would be to use a custom type converter
You could use something like
void ConvertUsing(Func<TSource, TDestination> mappingFunction);
And put in your own function that would resolve your ApplicationDriverEquipmentType by name, assuming name is unique like this:
var applicationEquipments = new ApplicationDriverEquipmentTypeRepository().FindAll(); // get all the values somehow from db
var dictionary = applicationEquipments.ToDictionary(x=>x.Name);
Func<string, ApplicationDriverEquipmentType> resolver = x=>dictionary[x];
Yet another way to do this would be to use a custom value resolver
Essentially, the idea would be the same - map of pre-loaded objects, only the way you "plug it in" would be different
Try to use MapFrom method instead:
.ForMember(c => c.Type.Name, p => p.MapFrom(s => Domain.StaticStrings.ApplicationDriverEquipmentTypeStaticStringsDomain.Tractor));

Categories

Resources