I want to avoid duplicating the src.Statuses.Where(s => s.StatusType!.StatusGroupId == 1).OrderByDescending(s => s.CreatedUTC).First() from the below mapping. Without changing the shape of my destinationDto.
I'm aware that I could change the DestinationDto to hold a "StatusDto" object which could then have it's own projection defined, and achieve it that way.
Does automapper have some syntax to do this without having to create extra dto's that reflect the source structure?
Basically a way to say
var status = src.Statuses.Where(s => s.StatusType!.StatusGroupId == 1).OrderByDescending(s => s.CreatedUTC).First()
and then use that status across multiple .ForCtorParam
The source looks like this:
Source 1 -- 0..N Status
Status 0..N -- 1 StatusType
StatusType 0..N -- 1 StatusGroup
public class DestinationDto
{
public DestinationDto(...)
public int Id { get; set; }
public DateTime StatusDate { get; set; }
public string Open { get; set; }
public string Status { get; set; }
public DateTime Created { get; set; }
}
public class DestinationProfile : Profile
{
public DestinationProfile()
{
CreateProjection<SourceType, DestinationDto>(MemberList.Destination)
.ForCtorParam(nameof(DestinationDto.Id), opt => opt.MapFrom(src => src.Id))
.ForCtorParam(nameof(DestinationDto.StatusDate),
opt => opt.MapFrom(src => src.Statuses.Where(s => s.StatusType!.StatusGroupId == 1).OrderByDescending(s => s.CreatedUTC).First().CreatedUTC))
.ForCtorParam(nameof(DestinationDto.Open),
opt => opt.MapFrom(src => src.Statuses.Where(s => s.StatusType!.StatusGroupId == 1).OrderByDescending(s => s.CreatedUTC).First().StatusType!.IsOpen))
.ForCtorParam(nameof(DestinationDto.Status),
opt => opt.MapFrom(src => src.Statuses.Where(s => s.StatusType!.StatusGroupId == 1).OrderByDescending(s => s.CreatedUTC).First().StatusType!.Label))
.ForCtorParam(nameof(DestinationDto.Created), opt => opt.MapFrom(src => src.CreatedUTC));
}
}
Related
I have got a problem using Automapper when conditionally Mapping a table.
Here is an example:
public class DepositsVm : IMapFrom<Deposits>
{
public long DepId { get; set; }
public AddressDto Address { get; set; }
public void Mapping(Profile profile)
{
profile.CreateMap<Deposits, DepositsVm>()
.ForMember(d => d.DepId, s => s.MapFrom(s => s.DepId))
.ForMember(d => d.Address, opt => opt.MapFrom(s => s.ProcessingId != null ? s.DataProcessing.GridCollect.Grid.Address : s.Reduction.DataCollect.Tower.Address));
}
}
This results in an Object reference error.
I can see we can use https://docs.automapper.org/en/stable/Conditional-mapping.html#preconditions but this allow to check for only one condition. I expect to map a table using different join condition in failure and success scenarios.
But this works, because I'm projecting each Address separately. But, this is not desired. Because both are from Same Address Table
public class DepositsVm : IMapFrom<Deposits>
{
public long DepId { get; set; }
public AddressDto GridAddress { get; set; }
public AddressDto TowerAddress { get; set; }
public void Mapping(Profile profile)
{
profile.CreateMap<Deposits, DepositsVm>()
.ForMember(d => d.DepId, s => s.MapFrom(s => s.DepId))
.ForMember(d => d.GridAddress, opt => opt.MapFrom(s => s.DataProcessing.GridCollect.Grid.Address));
.ForMember(d => d.TowerAddress, opt => opt.MapFrom(s => s.Reduction.DataCollect.Tower.Address));
}
}
Either some part of s.DataProcessing.GridCollect.Grid.Address is null, or some part of s.Reduction.DataCollect.Tower.Address is null
I have the following models:
public class Stuff
{
...
public IList<Place> Places { get; set; } = null!;
...
}
public class Place
{
...
public IList<Stuff> Stuffs { get; set; } = null!;
...
}
public class StuffEntity
{
...
public IList<PlaceStuffEntity> Places { get; set; } = null!;
...
}
public class PlaceEntity
{
...
public IList<PlaceStuffEntity> Stuffs { get; set; } = null!;
...
}
public class PlaceStuffEntity
{
public int StuffId { get; private set; }
public StuffEntity Stuff { get; set; } = null!;
public int PlaceId { get; private set; }
public PlaceEntity Place { get; set; } = null!;
}
cfg.CreateMap<StuffEntity, Stuff>()
.ForMember(d => d.Places,
opt => opt.MapFrom(s => s.Places.Select(y => y.Place).ToList()));
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Places.Select(y => y.Stuff).ToList()));
cfg.CreateMap<PlaceAndStuffEntity, Stuff>() // < -- Issue
.IncludeMembers(entity=> entity.Stuff);
cfg.CreateMap<PlaceAndStuffEntity, Place>() // < -- Issue
.IncludeMembers(entity=> entity.Place);
by some reason when I add both last lines, conversion does not work ...
But if I add only one line for example for converting PlaceAndStuffEntity -> Stuff works only one conversion from PlaceEntity -> Place
var place = mapper.Map<Place>(placeEntity); // <- This works
var stuff = mapper.Map<Stuff>(stuffEntity); // <- Does not work !!
Is there a way properly handle the following conversions ?
It sounds like you want to map through the joining table (PlaceAndStuff) to get to the other entity type. For instance in your Place to get a list of Stuff, and Stuff to get a list of Place, you want to direct Automapper how to navigate through the joining table.
For instance:
cfg.CreateMap<StuffEntity, Stuff>()
.ForMember(x => x.Places, opt => opt.MapFrom(src => src.PlaceEntity));
// Where StuffEntity.Places = PlaceAndStuffEntities, to map Stuff.Places use PlaceAndStuffs.PlaceEntity
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(x => x.Stuffs, opt => opt.MapFrom(src => src.StuffEntity));
So rather than trying to tell EF how to map the joining entity PlaceStuffEntity, we focus on the PlaceEntity and StuffEntity, and tell Automapper to navigate through the joining entity to get at the actual Stuff and Place relatives via the joining entity.
Change
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Places.Select(y => y.Stuff).ToList()));
to
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Stuffs.Select(y => y.Stuff).ToList()));
Source type PlaceEntity does not have a property named Places, only Stuffs.
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
I have a pretty basic Entity Framework entity that looks like this:
public class Student
{
public string Given { get; set; }
public string Surname { get; set; }
public ICollection<Address> Addresses { get; set; }
}
I'd like to use AutoMapper to map this entity to a corresponding flattened ViewModel that looks like this:
public class StudentViewModel
{
public string Given { get; set; }
public string Surname { get; set; }
public string PhysicalAddressStreet { get; set; }
public string PhysicalAddressCity { get; set; }
public string PhysicalAddressState { get; set; }
public string PostalAddressStreet { get; set; }
public string PostalAddressCity { get; set; }
public string PostalAddressState { get; set; }
}
For this I've tried the following mapping configuration:
CreateMap<Student, StudentViewModel>()
.ForMember(dest => dest.Given, opt => opt.MapFrom(src => src.Given))
.ForMember(dest => dest.Surname, opt => opt.MapFrom(src => src.Surname))
.ForMember(dest => dest.PhysicalAddressStreet, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Physical).Street))
.ForMember(dest => dest.PhysicalAddressCity, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Physical).City))
.ForMember(dest => dest.PhysicalAddressState, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Physical).State))
.ForMember(dest => dest.PostalAddressStreet, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Postal).Street))
.ForMember(dest => dest.PostalAddressCity, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Postal).City))
.ForMember(dest => dest.PostalAddressState, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Postal).State));
The problem is, when I run this mapping using projections:
studentDbSet.Where(st => st.Id == studentId)
.ProjectTo<TProjection>(_mapper.ConfigurationProvider);
I get the following error:
Dynamic SQL Error SQL error code = -104 Token unknown - line 14,
column 2 OUTER
This is a Firebird error, it seems that when compiling the Linq to SQL the query that is being generated includes OUTER APPLY, which is not supported in Firebird.
Is there any way to rework my projection to avoid the OUTER APPLY?
To the best of my knowledge, the OUTER APPLY is generated from the FirstOrDefault() call. Is there another way I can write the Linq to avoid using that?
Edit for clarification: This is a situation where I am not in a position to be able to modify the Entity or the database schema, so assume that those are untouchable.
I think you have a modeling problem at the core here. If you need the physical address, just include a PhysicalAddress property on the model, and maintain that relationship. You can still have the collection of addresses with the type. It looks like you're doing "FirstOrDefault", meaning either you can only have one physical address or only the first matters. I'm guessing it's that you can only have one.
So just have one. On the Student model (and Student table), have a FK to the Address table, "PhysicalAddress". Then in the places in the code you maintain addresses, update the PhysicalAddress appropriately. Encapsulating the child collection so that you can't do just any add/remove operation helps.
Once you have a PhysicalAddress relationship on the Student, this problem becomes trivial, it's just a normal mapping.
Here is the only way of writing the LINQ query that avoids OUTER APPLY (not sure how that can be mapped with AutoMapper, leaving that part for you if you really need it):
var query =
from student in studentDbSet
where student.Id == studentId
from physicalAddress in student.Addresses.Where(a => a.Type == AddressType.Physical)
from postalAddress in student.Addresses.Where(a => a.Type == AddressType.Postal)
select new StudentViewModel
{
Given = student.Given,
Surname = student.Surname,
PhysicalAddressStreet = physicalAddress.Street,
PhysicalAddressCity = physicalAddress.City,
PhysicalAddressState = physicalAddress.State,
PostalAddressStreet = postalAddress.Street,
PostalAddressCity = postalAddress.City,
PostalAddressState = postalAddress.State,
};
I have something like this:
public class DomainEntity
{
public string Name { get; set; }
public string Street { get; set; }
public IEnumerable<DomainOtherEntity> OtherEntities { get; set; }
public IEnumerable<DomainAnotherEntity> AnotherEntities { get; set; }
}
public class ApiEntity
{
public string Name { get; set; }
public string Street { get; set; }
public int OtherEntitiesCount { get; set; }
}
And following mapper configuration:
Mapper.Configuration.AllowNullCollections = true;
Mapper.CreateMap<DomainEntity, ApiEntity>().
ForSourceMember(e => e.OtherEntities, opt => opt.Ignore()).
ForSourceMember(e => e.AntherEntities, opt => opt.Ignore()).
ForMember(e => e.OtherEntitiesCount, opt => opt.MapFrom(src => src.OtherEntities.Count()));
Mapper.CreateMap<ApiEntity, DomainEntity>().
ForSourceMember(e => e.OtherEntitiesCount, opt => opt.Ignore()).
ForMember(e => e.OtherEntities, opt => opt.Ignore()).
ForMember(e => e.AnotherEntities, opt => opt.Ignore());
To get the ApiEntity from the DomainEntity I'm using var apiEntity = Mapper.Map<DomainEntity, ApiEntity>(myDomainEntity);
To get the merged DomainEntity from an ApiEntity I'm using var domainEntity = Mapper.Map(myApiEntity, myDomainEntity);
But when using this, the properties OtherEntities and AnotherEntities are set to null - even when they had values before calling the mapping from myApiEntity to myDomainEntity. How can I avoid this so they really merge and not just replacing values?
Thanks for any help.
I think you're looking for UseDestinationValue instead of Ignore:
Mapper.CreateMap<ApiEntity, DomainEntity>().
ForSourceMember(e => e.OtherEntitiesCount, opt => opt.UseDestinationValue()).
ForMember(e => e.OtherEntities, opt => opt.UseDestinationValue()).
ForMember(e => e.AnotherEntities, opt => opt.UseDestinationValue());