Avoiding OUTER APPLY in Entity Framework projections - c#

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,
};

Related

Automapper: How to convert a list of ids to a list of objects

I want to convert an array of ids from a dto to a list of related objects using AutoMapper, however, the mapping only works one way.
The current setup is the following:
public class Group
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Location> Locations { get; set; }
}
public class GroupDto
{
public int id { get; set; }
public string name { get; set; }
public int[] locationIds { get; set; }
}
And the code in the GroupProfile:
CreateMap<Group, GroupDto>()
.ForMember(dest => dest.id, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.name, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.locationIds, opt =>
opt.MapFrom(src => src.Locations.Select(l => l.Id).ToArray()));
This code works when I try to convert a Group to a GroupDto, but not when I try it the other way around. The following error occurs:
An unhandled exception has occurred while executing the request.
AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping.
Mapping types:
GroupDto -> Group
Also, when I do it without the AutoMapper like below, it just works:
Group group = new Group
{
Name = groupDto.name,
Locations = _context.Locations
.Where(l => groupDto.locationIds.Contains(l.Id))
.ToList()
};
Help is appreciated!
AutoMapper cannot convert an int[] to a ICollection<Location>. You need to create a seperate mapping for this.
CreateMap<GroupDto, Group>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.id))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.name))
.ForMember(dest => dest.Locations, opt => opt.MapFrom(src => src.Select(l => new Location { Id = src }));
I haven't tested this snippet so I hope you get the gist of it.
Your AutoMapper doesn't find the mapping from GroupDto to Group.
You need .ReverseMap() to support reverse mapping from Group to GroupDto.
Mapping configuration for mapping from Location to int and vice versa via .ConstructUsing().
Modify the mapping for the member locationIds.
The mapping for the members id and name may be omitted as AutoMapper will perform the mapping between members by ignoring case-sensitive.
CreateMap<Location, int>()
.ConstructUsing(src => src.Id);
CreateMap<int, Location>()
.ConstructUsing(src => new Location { Id = src });
CreateMap<Group, GroupDto>()
.ForMember(dest => dest.locationIds, opt => opt.MapFrom(src => src.Locations))
.ReverseMap();
Demo # .NET Fiddle

Automapper - Projecting multiple properties from conditionally selected list

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

How can I include a child object mapping using AutoMapper when the source objects have no inheritance relationship?

I have an object model something like this:
public class Concert {
public Band Band { get; set; }
public ConcertVenue Venue { get; set; }
}
public class TicketOrder {
public Concert Concert { get; set; }
public string CustomerName { get; set; }
}
// DTOs for email and web views:
public class ConcertDto {
public string Artist { get; set; }
public string Venue { get; set; }
}
public class TicketOrderDto : ConcertDto {
public string CustomerName { get; set; }
}
I'm using AutoMapper to map domain objects to DTOs. The DTOs here have an inheritance relationship that doesn't exist in the domain model (because when I send an email about a ticket order, I want to include all the information about the concert)
I have a mapping defined like this:
config.CreateMap<Concert, ConcertDto>()
.ForMember(dto => dto.Artist, opt => opt.MapFrom(concert => concert.Band.Name))
.ForMember(dto => dto.Venue, opt => opt.MapFrom(concert => concert.GetVenueSummary());
config.CreateMap<TicketOrder, ConcertDto>()
.ForMember(dto => dto.Artist, opt => opt.MapFrom(concert => concert.Band.Name))
.ForMember(dto => dto.Venue, opt => opt.MapFrom(concert => concert.GetVenueSummary())
.ForMember(dto => dto.CustomerName, optn.MapFrom(order => order.Customer.FullName))
;
There's some duplication in those maps, and what I want to do is to reuse the Concert > ConcertViewData mapping when I map the TicketOrderDto:
cfg.CreateMap<TicketOrder, TicketOrderDto>()
// This is what I *want* but isn't valid AutoMapper syntax:
.IncludeMembers(dto => dto, order => order.Concert)
.ForMember(dto => dto.CustomerName, optn.MapFrom(order => order.Customer.FullName));
but this fails with:
System.ArgumentOutOfRangeException: Only member accesses are allowed.
dto => dto (Parameter 'memberExpressions')
at AutoMapper.ExpressionExtensions.EnsureMemberPath(LambdaExpression exp, String name)
Calling .IncludeBase<> doesn't work, because ConcertOrder doesn't derive from Concert.
Is there an easy way to import one map into another but specify that it should map from a child object of the source type? i.e. "hey, please map source.Child onto this, and then run the regular source > this mapping?"
I am going to make an assumption here, but I believe the mapping should be from TicketOrder to TicketOrderDto, and not ConcertDto (which contains no CustomerName property) as the given models don't match the given mapping configuration.
In that case, you should be able to use .AfterMap() on ticket mapper configuration to map from Concert to ConcertDto.
cfg.CreateMap<TicketOrder, TicketOrderDto>()
.ForMember(d => d.CustomerName, o => o.MapFrom(s => s.Order.Customer.Name))
.AfterMap((s, d, context) => context.Mapper.Map(s.Concert, d));

AutoMapper condition Mapping Issue

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

AutoMapper, mapping one type in two different types

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.

Categories