Automapper Project.To breaks db IQueryable - c#

I have entities:
[Table("Representatives")]
public class Representative
{
[Column("RepID"), Key]
public int Id { get; set; }
[Column("MemID")]
public int MemberId { get; set; }
public virtual Member Member { get; set; }
}
[Table("Members")]
public class Member
{
[Column("MemID"), Key]
public int Id { get; set; }
[ForeignKey("MemberId")]
public virtual ICollection<Representative> Representatives { get; set; }
}
where Representatives relate to Member as many to one. I want to retrieve some representatives with their parent members and project it all to some view model. I've configured automapper like this:
Mapper.CreateMap<Representative, RepresentativeViewModel>()
.ForMember(m => m.RepId, a => a.MapFrom(x => x.Id))
.ForMember(m => m.MemberModel, a => a.MapFrom(x => x.Member));
Mapper.CreateMap<Member, MemberViewModel>()
.ForMember(m => m.MemId, a => a.MapFrom(x => x.Id))
.ForMember(m => m.OrganizationName, a => a.MapFrom(x => x.Name));
If I run this code:
var prj = ctx.Representatives.Where(r => r.Id == 1).Project().To<RepresentativeViewModel>();
then I get expression tree that contains func invocation (debugged):
...
.Lambda #Lambda2<System.Func`2[ConsoleApplication1.Representative,ConsoleApplication1.RepresentativeViewModel]>(ConsoleApplication1.Representative $var1)
{
.New ConsoleApplication1.RepresentativeViewModel(){
Name = $var1.Name,
RepId = $var1.Id,
MemberModel = .Invoke (.Lambda #Lambda3<System.Func`2[ConsoleApplication1.Member,ConsoleApplication1.MemberViewModel]>)($var1.Member)
}
}
.Lambda #Lambda3<System.Func`2[ConsoleApplication1.Member,ConsoleApplication1.MemberViewModel]>(ConsoleApplication1.Member $var2)
{
.New ConsoleApplication1.MemberViewModel(){
MemId = $var2.Id,
OrganizationName = $var2.Name
}
}
...
so for some reason builder invokes Func instead of calling Expression. Do you have ideas why it's so? How can I make my mapper build correct expression and SQL query?

Related

Automapper ignores the mapping for derived class

I have a class hierarchy as described below:
public partial class EmployeeDTO
{
public virtual long Id { get; set; }
public virtual string Name { get; set; }
public virtual string ReportsTo { get; set; }
}
public class ManagerDTO: EmployeeDTO
{
public override string ReportsTo
{
get => null;
set => throw new InvalidOperationException("ReportsTo can not be set to other than the preset value.");
}
}
public class EmployeeModel
{
public long Id { get; set; }
public string FullName { get; set; }
public EmployeeType Type { get; set; }
public string ReportsTo { get; set; }
}
public enum EmployeeType
{
Employee = 0,
Manager = 1,
}
Mapping configuration
CreateMap<EmployeeDTO, EmployeeModel>()
.ForMember(empmodel => empmodel.FullName, empdto => empdto.MapFrom(empdto => empdto.Name))
.ForMember(empmodel => empmodel.Type, empdto => empdto.MapFrom(empdto => EmployeeType.Employee));
CreateMap<ManagerDTO, EmployeeModel>()
.ForMember(model => model.FullName, dto => dto.MapFrom(dto => dto.Name))
.ForMember(model => model.Type, dto => dto.MapFrom(dto => EmployeeType.Manager));
CreateMap<EmployeeModel, ManagerDTO>()
.ForMember(dto => dto.Name, model => model.MapFrom(model => model.FullName))
.ForMember(t => t.ReportsTo, t => t.Ignore());
CreateMap<EmployeeModel, EmployeeDTO>()
.ForMember(dto => dto.Name, model => model.MapFrom(model => model.FullName));
I am trying to patch the entity as follows:
[HttpPatch]
public IActionResult Patch([FromBody] Delta<EmployeeDTO> entityDelta)
{
var existingModel = new EmployeeModel
{
Id = 123,
FullName = "XYZ",
Type = Shared.EmployeeType.Manager,
ReportsTo = null
};
var existingEntity = _mapper.Map<EmployeeDTO>(existingModel);
entityDelta.Patch(existingEntity);
// Following mapping incorrectly sets type of updatedModel as EmployeeType.Employee
var updatedModel = _mapper.Map(existingEntity, existingModel);
return Ok(updatedModel);
}
After executing the above code expected type of the updatedModel to be Manager, but the type is Employee instead.
I have tried using the other overloads for the Map. Map(entity) works as expected when explicit type is provided for the previous mapping ie. Map(model). However, this overload creates another instance of the dbmodel with the same id, which leads to further issues.
To avoid having two runtime DbEntries with the same id, I am trying to use the Map(entity, existimgModel) overload of the mapper, which is failing to give the desired outcome.
Any ideas on how to solve this?
Sample code

How to assign null for the object if object members have no value - automapper c#

I am using automapper in c#.
class A
{
public int Value { get; set; }
public string Code { get; set; }
public B? Details { get; set; }
}
class B
{
public int Id { get; set;}
public string Name { get; set; }
}
class C
{
public int Value { get; set; }
public string Code { get; set; }
public int? DetailId { get; set; }
public string? DetailName { get; set; }
}
And in automapper I used like below:
CreateMap<C, A>()
.ForPath(o => o.Details.Id, b => b.MapFrom(z => z.DetailId))
.ForPath(o => o.Details.Name, b => b.MapFrom(z => z.DetailName))
.ReverseMap();
When I using like the above mapping, I get like output as
"details ": {
"id": 0,
"name": ""
}
I need to get Details value as null instead of an object type if it's members have no value. i.e) DetailId and DetailName have no value. How to get this?
"details" : null
You can user Conditional Mapping
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<C, B>()
.ForMember(o => o.Id, b => b.MapFrom(z => z.DetailId))
.ForMember(o => o.Name, b => b.MapFrom(z => z.DetailName));
cfg.CreateMap<C, A>()
.ForMember(o => o.Details, b => b.MapFrom((c, a, obj, context) => !string.IsNullOrEmpty(c.DetailName) ? context.Mapper.Map<B>(c) : null))
.ReverseMap();
});
You could do this with an AutoMapper after map action.
Something like this:
CreateMap<C, A>()
.ForPath(o => o.Details.Id, b => b.MapFrom(z => z.DetailId))
.ForPath(o => o.Details.Name, b => b.MapFrom(z => z.DetailName))
.AfterMap((src, dest) =>
{
dest.Details = src.DetailId.HasValue && src.DetailName != null
? dest.Details
: null;
})
.ReverseMap());
You can use IValueResolver interface to achieve what you require. Documentation: http://docs.automapper.org/en/stable/Custom-value-resolvers.html
Also there's a similar question: Automapper Mapping Multiple Properties to Single Property
Configuration:
CreateMap<C, A>()
.ForMember(o => o.Details, b => b.MapFrom<DetailsValueResolver>())
.ReverseMap();
Implementation:
// Note: this does not cover ReverseMap() when you would try to convert A to C
public class DetailsValueResolver : IValueResolver<C, A, B>
{
// Runs every time you map C to A
public B Resolve(C source, A destination, B destMember, ResolutionContext context)
{
// Covers cases where you can get null or empty DetailName, as well as null or zero DetailId
if (!string.IsNullOrEmpty(source.DetailName) && source.DetailId > 0)
{
return new B { Id = (int)source.DetailId, Name = source.DetailName };
}
return null;
}
}
You also can omit explicitly setting strings and classes as nullable types with ? as you do here:
public B? Details { get; set; }
public string? DetailName { get; set; }
Because string type and any class is null by default.

Can null check cause client side evaluation?

I'd want to ask why query like this is being evaluated on the client side:
_context
.Items
.Include(x => x.Status)
.Include(x => x.Orders)
.ThenInclude(x => x.User)
.Include(x => x.Orders)
.ThenInclude(x => x.OrderStatus)
.Where(x => x.Orders.Any())
.Where(x => x.Order != null)
.Where(x => x.Order.User.SomeProperty.ToLower() == user.SomeProperty.ToLower());
where user used in user.SomeProperty.ToLower() is just Identity user and it isn't null.
public class Item
{
public Guid Id = { get; protected set; }
public List<Order> Orders { get; set; } = new List<Order>();
public Order Order => Orders.FirstOrDefault(x => x.OrderStatus.Name = "Active");
public Status Status { get; set; }
}
public class Order
{
public Guid Id = { get; protected set; }
public User User = { get; set; }
public Status OrderStatus = { get; set; }
}
public class Status
{
public Guid Id = { get; protected set; }
public string Name = { get; set; }
}
EF Core warnings say that null check is one of the reasons, but I cannot understand why would null check cannot be translated
warn: Microsoft.EntityFrameworkCore.Query[20500]
The LINQ expression 'where (Property([x].Order, "Id") != null)' could not be translated and will be evaluated locally.
warn: Microsoft.EntityFrameworkCore.Query[20500]
The LINQ expression 'where ([x].Order.User.SomeProperty==__user_SomeProperty_0)' could not be translated and will be evaluated locally.
EF can not translate query in methods or properties. Move this
public Order Order => Orders.FirstOrDefault(x => x.OrderStatus.Name = "Active");
To the actual query
edit. You can use extension methods to reuse queries instead
edit2: Create an extension method
public static class FooQueryExtensions
{
public static IQueryable<FooResult> MyFooQuery(this IQueryable<Foo> source)
{
return source.SelectMany(...).Where(...); //basicly do what yuo want
}
}
used like
_context.Set<Foo>().MyFooQuery().Where(result => more query);

Include collection in Entity Framework Core

For example, I have those entities:
public class Book
{
[Key]
public string BookId { get; set; }
public List<BookPage> Pages { get; set; }
public string Text { get; set; }
}
public class BookPage
{
[Key]
public string BookPageId { get; set; }
public PageTitle PageTitle { get; set; }
public int Number { get; set; }
}
public class PageTitle
{
[Key]
public string PageTitleId { get; set; }
public string Title { get; set; }
}
How should I load all PageTitles, if I know only the BookId?
Here it is how I'm trying to do this:
using (var dbContext = new BookContext())
{
var bookPages = dbContext
.Book
.Include(x => x.Pages)
.ThenInclude(x => x.Select(y => y.PageTitle))
.SingleOrDefault(x => x.BookId == "some example id")
.Pages
.Select(x => x.PageTitle)
.ToList();
}
But the problem is, that it throws exception
ArgumentException: The properties expression 'x => {from Pages y
in x select [y].PageTitle}' is not valid. The expression should represent
a property access: 't => t.MyProperty'. When specifying multiple
properties use an anonymous type: 't => new { t.MyProperty1,
t.MyProperty2 }'. Parameter name: propertyAccessExpression
What's wrong, what exactly should I do?
Try accessing PageTitle directly in ThenInclude:
using (var dbContext = new BookContext())
{
var bookPages = dbContext
.Book
.Include(x => x.Pages)
.ThenInclude(y => y.PageTitle)
.SingleOrDefault(x => x.BookId == "some example id")
.Select(x => x.Pages)
.Select(x => x.PageTitle)
.ToList();
}

Fluent NHibernate "Could not resolve property"

I have read a lot of the questions about that same error but none since to match my exact problem. I'm trying to access the property of an object, itself part of a root object, using Fluent NHibernate. Some answers say I need to use projections, others that I need to use join, and I think it should work through lazy loading.
Here are my two classes along with the Fluent mappings:
Artist class
public class Artist
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Album> Albums { get; set; }
public virtual string MusicBrainzId { get; set; }
public virtual string TheAudioDbId { get; set; }
public Artist() { }
}
public class ArtistMap : ClassMap<Artist>
{
public ArtistMap()
{
LazyLoad();
Id(a => a.Id);
Map(a => a.Name).Index("Name");
HasMany(a => a.Albums)
.Cascade.All();
Map(a => a.MusicBrainzId);
Map(a => a.TheAudioDbId);
}
}
Album class
public class Album
{
public virtual int Id { get; set; }
public virtual Artist Artist { get; set; }
public virtual string Name { get; set; }
public virtual IList<Track> Tracks { get; set; }
public virtual DateTime ReleaseDate { get; set; }
public virtual string TheAudioDbId { get; set; }
public virtual string MusicBrainzId { get; set; }
public Album() { }
}
public class AlbumMap : ClassMap<Album>
{
public AlbumMap()
{
LazyLoad();
Id(a => a.Id);
References(a => a.Artist)
.Cascade.All();
Map(a => a.Name).Index("Name");
HasMany(a => a.Tracks)
.Cascade.All();
Map(a => a.ReleaseDate);
Map(a => a.TheAudioDbId);
Map(a => a.MusicBrainzId);
}
}
And the error happens when this code is interpreted:
var riAlbum = session.QueryOver<Album>()
.Where(x => x.Name == albumName && x.Artist.Name == artist)
.List().FirstOrDefault();
The error happens when Fluent NHibernate tries to resolve the x.Artist.Name value:
{"could not resolve property: Artist.Name of: Album"}
What would be the correct way of doing this?
You have to think of your QueryOver query as (nearly) directly translating into SQL. With this in mind, imagine this SQL query:
select
Album.*
from
Album
where
Album.Name = 'SomeAlbumName' and
Album.Artist.Name = 'SomeArtistName'
This won't work because you can't access a related table's properties like that in a SQL statement. You need to create a join from Album to Artist and then use a Where clause:
var riAlbum =
session.QueryOver<Album>()
.Where(al => al.Name == albumName)
.JoinQueryOver(al => al.Artist)
.Where(ar => ar.Name == artistName)
.List()
.FirstOrDefault();
Also, since you're using FirstOrDefault, you may want to consider moving that logic to the database end. Currently, you're pulling back every record matching your criteria and then taking the first one. You could use .Take to limit the query to 1 result:
var riAlbum =
session.QueryOver<Album>()
.Where(al => al.Name == albumName)
.JoinQueryOver(al => al.Artist)
.Where(ar => ar.Name == artistName)
.Take(1)
.SingleOrDefault<Album>();
Another explanation is that you are missing your mapping of this property or field in a NHibernateClassMapping definition. I came here about why I was getting this error based on the following scenario.
var query = scheduleRepository.CurrentSession().Query<Schedule>()
.Where(x => x.ScheduleInfo.StartDate.Date < dateOfRun.Date);
This was giving me a Could Not Resolve Property error for StartDate. This was a head scratcher, since I use this syntax all the time.
My mapping file was the following:
public class ScheduleInfoMapping : NHibernateClassMapping<ScheduleInfo>
{
public ScheduleInfoMapping()
{
DiscriminateSubClassesOnColumn("Type");
Map(x => x.Detail).MapAsLongText();
}
}
which was missing the StartDate. Changed to:
public class ScheduleInfoMapping : NHibernateClassMapping<ScheduleInfo>
{
public ScheduleInfoMapping()
{
DiscriminateSubClassesOnColumn("Type");
Map(x => x.Detail).MapAsLongText();
Map(x => x.StartDate);
}
}
Which resolved the error.

Categories

Resources