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)));
});
Related
This is my first real attempt using Automapper and I'm struggling to properly map a many-to-many relationship using DTOs.
Here are the models:
public class Camp
{
[Key]
public long Id { get; set; }
[Required]
[MaxLength( 150 )]
public string Name { get; set; }
[Required]
[MaxLength( 150 )]
public string Location { get; set; }
[Required]
public DateTime StartDate { get; set; }
[NotMapped]
public int CampYear
{
get => StartDate.Year;
}
public bool Archived { get; set; }
public ICollection<Application> Applications { get; set; }
public ICollection<CampStaffPosition> CampStaffPositions { get; set; }
}
public class StaffPosition
{
[Key]
public int Id { get; set; }
public string PositionName { get; set; }
public ICollection<CampStaffPosition> CampStaffPositions { get; set; }
}
public class CampStaffPosition
{
public long CampId { get; set; }
public Camp Camp { get; set; }
public int StaffPositionId { get; set; }
public StaffPosition StaffPosition { get; set; }
public short PositionQuantity { get; set; } // Additional Info
}
And the DTOs I'm trying to map to:
public class CampDto
{
public long Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public DateTime StartDate { get; set; }
public int CampYear { get; }
public bool Archived { get; set; }
public ICollection<ApplicationDto> Applications { get; set; }
public ICollection<StaffPositionDto> Positions { get; set; } // Through CampStaffPositions
}
public class StaffPositionDto
{
public int Id { get; set; }
public string Type { get; set; }
public string PositionName { get; set; }
public short PositionQuantity { get; set; } // From CampStaffPositions
}
After reading several of the other SO posts and trying to follow their examples, I've come up short. Here are a couple different mapping attempts:
CreateMap<Camp, CampDto>()
.ForMember( d => d.Positions, opt => opt.MapFrom( d => d.CampStaffPositions.Select( d => d.StaffPosition ).ToList() ) );
CreateMap<StaffPosition, CampDto>()
.ForMember( pr => pr.Positions, opt => opt.MapFrom( cp => cp.PositionName ) );
CreateMap<StaffPosition, StaffPositionDto>();
//CreateMap<StaffPosition, StaffPositionDto>()
// .ForMember( cr => cr.PositionQuantity, opt => opt.MapFrom( c => c.CampStaffPositions ) );
These are the most recent errors that I'm getting (with the commented line included):
Unable to create a map expression from StaffPosition.CampStaffPositions (System.Collections.Generic.ICollection`1[Server.Models.CampStaffPosition]) to StaffPositionDto.PositionQuantity (System.Int16)
Mapping types: StaffPosition -> StaffPositionDto Server.Models.StaffPosition -> Shared.Dto.Core.StaffPositionDto
Type Map configuration: StaffPosition -> StaffPositionDto Server.Models.StaffPosition -> Shared.Dto.Core.StaffPositionDto Destination Member: PositionQuantity
and with the commented line excluded:
Expression of type 'System.Collections.Generic.List`1[Server.Models.StaffPosition]' cannot be used for parameter of type 'System.Linq.IQueryable`1[Server.Models.StaffPosition]' of method 'System.Linq.IQueryable`1[Shared.Dto.Core.StaffPositionDto] Select[StaffPosition,StaffPositionDto](System.Linq.IQueryable`1[Server.Models.StaffPosition], System.Linq.Expressions.Expression`1[System.Func`2[Server.Models.StaffPosition,Shared.Dto.Core.StaffPositionDto]])'
How can I map the many-to-many to include the additional property from the join table without having to include the join table in my DTOs?
You need to flatten a complex object. You have properties in child objects, which you want to bring up one level higher, while still leveraging AutoMapper mapping capabilities. There is a method called IncludeMembers() (see the docs) that exists precisely for such case. It allows you to reuse the configuration in the existing maps for the child types, that way PositionName will be included from a child object StaffPosition acting as a second source when mapping from CampStaffPosition to StaffPositionDto:
config.CreateMap<Camp, CampDto>()
.ForMember(d => d.Positions, o => o.MapFrom(s => s.CampStaffPositions));
config.CreateMap<StaffPosition, StaffPositionDto>();
config.CreateMap<CampStaffPosition, StaffPositionDto>()
.IncludeMembers(p => p.StaffPosition);
config.CreateMap<Application, ApplicationDto>();
Usage:
var result = mapper.Map<List<CampDto>>(campsFromDatabase);
or using ProjectTo():
var result = await dbContext
.Set<Camp>()
.ProjectTo<CampDto>(mapper.ConfigurationProvider)
.ToListAsync();
All Positions have a Location (Many-to-1).
Different Location types have different Position types
Model:
public abstract class Location
{
public int Id { get; set; }
public string Name { get; set; }
public int AreaId { get; set; }
public Area Area { get; set; }
public byte[] ConcurrencyToken { get; set; }
}
public abstract class Location<T> : Location where T : Position
{
public ICollection<T> Positions { get; set; } = new List<T>();
}
public class Bay : Location<BayRow> {}
public class StandardLocation : Location<Position> {}
public class Position
{
public int Id { get; set; }
public int? Place { get; set; }
public int LocationId { get; set; }
public Location Location { get; set; }
public byte[] ConcurrencyToken { get; set; }
}
public class BayRow : Position
{
public int? Row { get; set; }
}
The above is abbreviated, there are many more implementations of each. All locations extend the generic class.
Mapping:
modelBuilder.Entity<Position>(entity =>
{
entity.ToTable("Position")
.HasDiscriminator<int>("Type")
.HasValue<Position>(1)
.HasValue<BayRow>(2);
entity.Property(x => x.ConcurrencyToken).IsConcurrencyToken();
//THIS IS THE ISSUE*
entity.HasOne(x => x.Location as Location<Position>).WithMany(x => x.Positions).HasForeignKey(x => x.LocationId);
});
modelBuilder.Entity<Location>(entity =>
{
entity.HasIndex(x => new {x.Name, x.AreaId}).IsUnique(true);
entity.Property(x => x.ConcurrencyToken).IsConcurrencyToken();
entity.HasDiscriminator<int>("Type")
.HasValue<StandardLocation>(1)
.HasValue<Bay<BayRow>>(2)
});
modelBuilder.Entity<Bay<BayRow>>(entity =>
{
entity.HasMany(x => x.Positions).WithOne(x => x.Location as Bay<BayRow>)
.HasForeignKey(x => x.LocationId).OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<BayRow>(entity =>
{
entity.Property(x => x.Row).HasColumnName("Row");
});
*The non-generic Location does not have positions
I've tried adding the collection to the base Location purely for mapping, to avoid ef duplicating/aliasing each location Impl i.e. BayId as LocationId.
publiic ICollection<Position> Positions { get; set; }
and with the new keyword to hide the base collection, but ef projects 2 collections...
public new ICollection<T> Positions { get; set; }
Any insight would be much appreciated.
I'm not sure this is supported by Entity Framework, without generating two tables - one for Bay and one for StandardLocation.
You might try this as a workaround.
public interface ITypedPosition<T> where T: Position
{
IEnumerable<T> Positions { get; }
}
public abstract class Location
{
public int Id { get; set; }
public string Name { get; set; }
public int AreaId { get; set; }
public Area Area { get; set; }
public byte[] ConcurrencyToken { get; set; }
public ICollection<Position> Positions { get; set; }
}
public class Bay : Location, ITypedPosition<BayRow>
{
IEnumerable<BayRow> ITypedPosition<BayRow>.Positions => base.Positions.OfType<BayRow>();
}
public class StandardLocation : Location, ITypedPosition<Position>
{
IEnumerable<Position> ITypedPosition<Position>.Positions => base.Positions.OfType<Position>();
}
I have two classes called Participant and Screen.
public class Participant
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public ICollection<Screen> Screens { get; set; }
}
public class Screen
{
public int Id { get; set; }
public DateTime? SignedDateTime { get; set; }
}
And I use a DTO as below:
public class ParticipantForDashboardDto
{
public int Id { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public DateTime? ScreenDateTime { get; set; }
public ICollection<Screen> Screens { get; set; }
}
In here, since I don't have ScreenDateTime field in my Participant class, I decided to get this field from Screen class using AutoMapper. Here is my MappingProfile:
CreateMap<Participant, ParticipantForDashboardDto>()
.ForMember(dest => dest.ScreenDateTime, opt => {
opt.MapFrom(src => src.Screens.Select(x => x.SignedDateTime));
});
This returns 0001-01-01T00:00:00. I suspect that the SELECT query is not the one I should use. How can I map SignedDateTime from Screen class to ScreenDate in the dto?
Your currently using a queryable, which will return multiple result, you probably on need one, so you need to create a way to resolve the proper one, e.g sort and first or default:
CreateMap<Participant, ParticipantForDashboardDto>()
.ForMember(dest => dest.ScreenDateTime, opt => {
opt.MapFrom(src => src.Screens.Select(x => x.SignedDateTime)
.OrderByDescending(x => x)
.FirstOrDefault());
});
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);
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();