Automapper configure 2 classes to one - c#

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

Related

Automapper: Trying to map to a property with a private setter

I am trying to map to a class that contains a property with a private setter. I read in a different answer that this was possible (Automapper apparently used Reflection to do so) just as long as I configured the property in question with the ForMember method (see below)
Mapper config
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<IDbCoverage, ICoverage>()
.ForMember(dest => dest.CoverageCodeDesc, conf => conf.MapFrom(src => src.CoverageCodeDesc));
});
Interfaces
public interface IDbCoverage
{
string ExternalMemberId { get; set; }
string CoverageCode { get; set; }
string CoverageCodeDesc { get; }
}
public interface ICoverage
{
string ExternalMemberId { get; set; }
string CoverageCode { get; set; }
string CoverageCodeDesc { get; }
}
Instantiation of the destination class:
public class Coverage
{
public string ExternalMemberId { get; set; }
public string CoverageCode { get; set; }
public string CoverageCodeDesc { get; private set; }
}
However when I try this only the properties with public setters are mapped. The source value of the other property is not mapped and the destination class ends up with a null value.
What am I doing wrong?

Automapper Many-to-Many using DTOs

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

AutoMapper: map nested object to flat

I have defined a mapping from one type to DTO. Another type references the first type as an property, but the output should be an flattened DTO that should use the already defined mapping for the first type.
class Program {
static void Main(string[] args) {
var mapperConfiguration = new MapperConfiguration(cfg => {
cfg.CreateMap<FirstDataType,
FirstTypeDto>().ForMember(d => d.TypeResult, opt => opt.MapFrom(s => s.ToString()));
/* HOW TO CONFIGURE MAPPING OF THE 'FirstData' PROPERTY TO USE THE ABOVE DEFINED MAPPING
cfg.CreateMap<SecondDataType, SecondTypeDto>()
*/
});
var firstData = new FirstDataType {
TypeName = "TestType",
TypeValue = "TestValue"
};
var secondData = new SecondDataType {
Id = 1,
Name = "Second type",
FirstData = firstData
};
var mapper = mapperConfiguration.CreateMapper();
var firstDto = mapper.Map<FirstTypeDto>(firstData);
var secondDto = mapper.Map<SecondTypeDto>(secondData);
Console.ReadKey(true);
}
}
public class FirstDataType {
public string TypeName {
get;
set;
}
public string TypeValue {
get;
set;
}
public override string ToString() {
return $ "{TypeName}: {TypeValue}";
}
}
public class SecondDataType {
public int Id {
get;
set;
}
public string Name {
get;
set;
}
public FirstDataType FirstData {
get;
set;
}
}
public class FirstTypeDto {
public string TypeName {
get;
set;
}
public string TypeValue {
get;
set;
}
public string TypeResult {
get;
set;
}
}
public class SecondTypeDto: FirstTypeDto {
public int Id {
get;
set;
}
public string Name {
get;
set;
}
}
How should I configure mapping for the second type to use the defined mapping for the property 'FirstData'?
Thanks!
First, credit goes to Lucian Bargaoanu for leading me in the right direction.
Basically, you need to create a mapping from the source to the destination derived type, but just include the existing mapping.
cfg.CreateMap<FirstDataType, SecondTypeDto>()
.IncludeBase<FirstDataType, FirstTypeDto>()
.ReverseMap();
cfg.CreateMap<SecondDataType, SecondTypeDto>()
.IncludeMembers(s => s.FirstData)
.ReverseMap();

Automapper says Missing map from System.String to System.Char? But I see no Char property

I've got three classes:
public class UserReport : Entity
{
public string Name { get; set; }
public string Email { get; set; }
public List<string> Departments { get; set; }
public List<string> Titles { get; set; }
}
public abstract class Entity
{
public Guid Id { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateLastModified { get; set; }
public string CreatedBy { get; set; }
public string LastModifiedBy { get; set; }
public bool Active { get; set; }
protected Entity()
{
DateCreated = DateTime.Now;
DateLastModified = DateTime.Now;
Id = Guid.NewGuid();
}
}
public class UserModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Departments { get; set; }
public string Titles { get; set; }
}
With my automapper configs set as:
CreateMap<List<string>, string>().ConvertUsing(strings => {
if (strings != null)
return string.Join("\n", strings);
return "";
});
CreateMap<UserReport, UserModel>();
When trying to call from a generic method using the Automapper Ef Extensions:
IQueryable<TModel> modelQueryable = _reportService.GetReportBaseQuery().ProjectToQueryable<TModel>();
I get this error
Missing map from System.String to System.Char. Create using Mapper.CreateMap.
GetReportBaseQuery() returns an IQueryable<TReport>, so the UserReport in this instance. I don't see any char properties, why is this coming up?
Just for testing I tried to make one:
CreateMap<String, Char>().ConvertUsing(x => x.FirstOrDefault());
And then it says:
Argument types do not match
Further research shows that this works:
Mapper.Map<List<TReport>, List<TModel>>(_reportService.GetReportBaseQuery().ToList());
But I can't use that since I need it to be a queryable returned. So something is different when I try to do an EF projection, not sure what that is though. Writing a select statement from one to the other is easy enough, but that's not generic and re-usable.
The solution is to explicitly set mapping for the Departments and Titles fields:
CreateMap<UserReport, UserModel>()
.ForMember(x => x.Departments, o => o.MapFrom(s => string.Join("\n", s.Departments)))
.ForMember(x => x.Titles, o => o.MapFrom(s => string.Join("\n", s.Titles)));
But, this does not explain, why this situation occurs.
As DOTang pointed out:
LINQ to Entities does not recognize the method 'System.String Join(System.String, System.Collections.Generic.IEnumerable1[System.String])' method, and this method cannot be translated into a store expression.
The AutoMapper extension seems to try to map the following thing:
IEnumerable<string> => IEnumerable<char>

Flatten Nested List using AutoMapper

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

Categories

Resources