EF Eager fetching derived class - c#

I´m using EF6 and trying to eager fetch the whole structure of an object. The problem is that i´m using inheritance.
Let´s say that i have this classes.
DbContext
DbSet<A> A { get; set; }
Example classes
public class A
{
public string Id { get; set; }
public IList<Base> Bases { get; set; }
}
public abstract class Base
{
public int Id { get; set; }
public string Name { get; set; }
}
public abstract class Base1 : Base
{
public SomeClass SomeClass { get; set; }
}
public class Base2 : Base1
{
}
public class Base3 : Base1
{
public SomeOtherClass SomeOtherClass { get; set; }
}
The error i get is:
The Include path expression must refer to a navigation property defined on the type.
Use dotted paths for reference navigation properties and the Select operator for collection navigation properties.
Why doesn´t it work with the following ?
public IEnumerable<A> GetAll(string id)
{
return _ctx.A
.Include(x => x.Bases.OfType<Base1>().Select(y=>y.SomeClass))
.Where(x => x.Id.Equals(id)).ToList();
}
New example
public IEnumerable<A> GetAll(string id)
{
var lists = _dbContext.A.Where(x => x.Id == id);
lists.SelectMany(a => a.Bases).OfType<Base1>().Include(e=>e.SomeClass).Load();
lists.SelectMany(b => b.Bases).OfType<Base3>().Include(e => e.SomeOtherClass).Load();
return lists;
}
EDIT: Added a new example that seems to work.

Shortly, it's not possible out of the box.
The only workaround I can suggest is to materialize the master query result, then execute several OfType queries with the necessary Includes using the same filter as the master query, and rely on EF navigation property fixup.
It requires an inverse navigation property in the Base class:
public abstract class Base
{
// ...
public A A { get; set; }
}
Then you can use something like this:
public IEnumerable<A> GetAll(string id)
{
var a = _ctx.A.Where(x => x.Id == id).ToList();
_ctx.Base.OfType<Base1>().Include(e => e.SomeClass).Where(e => e.A.Id == id).Load();
_ctx.Base.OfType<Base3>().Include(e => e.SomeOtherClass).Where(e => e.A.Id == id).Load();
return a;
}
The same idea can be used w/o inverse navigation property but with using the returned base Ids as filter:
public IEnumerable<A> GetAll(string id)
{
var a = _ctx.A.Include(e => e.Bases)
.Where(x => x.Id == id).ToList();
var baseIds = a.SelectMany(e => e.Bases.OfType<ModelA.Base1>().Select(b => b.Id));
db.Base.OfType<Base1>().Include(e => e.SomeClass)
.Where(e => baseIds.Contains(e.Id)).Load();
baseIds = a.SelectMany(e => e.Bases.OfType<Base3>().Select(b => b.Id));
db.Base.OfType<Base3>().Include(e => e.SomeOtherClass)
.Where(e => baseIds.Contains(e.Id)).Load();
return a;
}

Your problem is not in the Select(y=>y.SomeClass) it self, if you try to remove it from your query and execute your query again, you will get same problem. You cannot query the inherited type as child and you expect from entity framework to take care for everything.
If you look to your database, the table Base has a reference to A which is relation 1-many from A to Base.
you can either get all the Base entities where A.Id = something, by adding a navigational property A in the class Base, and in your DbContext you add DbSet<Base> Bases{get;set;} then your query will look like this
var details = _ctx.Bases.OfType<Base1>()
.Include(t=>t.Box)
.Include(t=>t.SomeClass)
.Where(t=>t.Box.Id ==something);
Other option, to use a DTO, in the below sample I used Anonymous type, but you can create a strongly DTO typed to meet your requirements.
var details = _ctx.A
.Where (t=>t.Id ==something)
.Select(a => new {
Id = a.Id,
// ... other A properites ,
Bases = _ctx.Bases.OfType<Base1>().Select(m=> new {
Id = m.Id,
Name = m.Name,
SomeClass = m.SomeClass
});
}
Hope this will help you

Related

Optional loading of EF Core computed properties

I am working on mapping a few database entities for a reporting tool.
At the moment, there are a few computed properties depending on navigation properties for their loading. They've been bound through AutoMapper to ease the process.
public class Customer
{
public long Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Foo> Foos { get; set; }
public virtual ICollection<Bar> Bars { get; set; }
}
public class CustomerDto
{
public long Id { get; set; }
public string Name { get; set; }
public long TotalNumberOfFoos { get; set; }
public long NumberOfBarsWithCondition { get; set; }
}
public class CustomerProfile : Profile
{
public CustomerProfile()
{
CreateMap<Customer, CustomerDto>()
.ForMember(d => d.TotalNumberOfFoos, p => p.MapFrom(c => c.Foos.Count))
.ForMember(d => d.NumberOfBarsWithCondition, p => p.MapFrom(c => c.Bars.Where(b => b.BarProperty == "something").Count()));
}
}
public class CustomerController : Controller
{
public async Task<List<CustomerDto>> CustomersByName(string name)
{
using (var db = new MyDbContext())
{
return await db.Customers
.ProjectTo<CustomerDto>(_mapper.ConfigurationProvider)
.Where(c => c.Name == name).ToListAsync();
}
}
}
Of course, the queries to retrieve these properties can become quite expensive as the size of the database increases, and they're not always needed in the final report.
The idea is to have an option for the user to choose if they want them included or not in the final report, but I haven't found a way to make the mapping optional at query time.
Is there a way to do this automatically, or am I forced to materialize the list and query these properties myself separately, losing the advantage of having computed properties from the database?
What you need is to utilize the so called AutoMapper Explicit expansion feature. Which should probably be called "explicit property inclusion" (not to be mixed with EF Core Include which is only for navigations), because it works for any destination property, and what it does it to rather include it automatically in the generated projection (Select), include it only when you opt-in explicitly.
So, you need first to configure such properties as ExplicitExpansion(), e.g.
CreateMap<Customer, CustomerDto>()
.ForMember(d => d.TotalNumberOfFoos, p =>
{
p.MapFrom(c => c.Foos.Count);
p.ExplicitExpansion();
})
.ForMember(d => d.NumberOfBarsWithCondition, p =>
{
p.MapFrom(c => c.Bars.Where(b => b.BarProperty == "something").Count());
p.ExplicitExpansion();
});
Now by default they won't be populated. Use the additional arguments of ProjectTo to pass which ones you want to "expand" (include), e.g.
.ProjectTo<CustomerDto>(_mapper.ConfigurationProvider, e => e.TotalNumberOfFoos)

How to map a collection inside a object with automapper

I don't know how to map 'Poses' which is inside my dto coming into my controller.
My DTO looks like this
public class NewInstructorProgramDto
{
public ICollection<int> Poses { get; set; }
}
I'm have an object that looks like this below where I need to map my DTO to this
public class InstructorProgram : BaseEntity
{
public ICollection<InstructorProgramPose> Poses { get; set; }
}
where InstructorProgramPose looks like this
public class InstructorProgramPose : BaseEntity
{
public int PoseId { get; set; }
public Pose Pose { get; set; }
public int InstructorProgramId { get; set; }
public InstructorProgram InstructorProgram { get; set; }
}
In my controller I map it like this
var newInstructorProgram = _mapper.Map<NewInstructorProgramDto, InstructorProgram>(newInstructorProgramDto);
and in my mapping file I need to figure out how to map all the integers for Poses to a collection of 'InstructorProgramPose' objects, where the integer is the PoseId
Here is what I have so far but this won't work because it only maps a single InstructorProgramPose, not a collection
CreateMap<NewInstructorProgramDto, InstructorProgram>()
.ForMember(d => d.Poses, o => o.MapFrom(s => new InstructorProgramPose() { PoseId = s.}));
I think I'm on the right track with this code below
CreateMap<NewInstructorProgramDto, InstructorProgram>()
.ForMember(d => d.Poses, o => o.MapFrom(s => s.Poses));
CreateMap<ICollection<int>, ICollection<InstructorProgramPose>>()
.ForMember(d => d.Select(i => i.PoseId), o => o.MapFrom(s => s.Select(i => i)));
but the debugger is throwing an error that says
Exception has occurred: CLR/AutoMapper.AutoMapperConfigurationException
An exception of type 'AutoMapper.AutoMapperConfigurationException' occurred in AutoMapper.dll but was not handled in user code: 'Custom configuration for members is only supported for top-level individual members on a type.'
UPDATE - Just to give you an idea of what I'm trying to do in Automapper, I've included the code I used to create and fill the object without Automapper, just to show what's needed.
In my controller
var newInstructorProgram = new InstructorProgram() {
Name = newInstructorProgramDto.Name,
Description = newInstructorProgramDto.Description,
Length = (EventLength)newInstructorProgramDto.Length,
Experience = (Experience)newInstructorProgramDto.Experience,
Style = (YogaStyle)newInstructorProgramDto.Style,
Calories = newInstructorProgramDto.Calories,
InstructorId = userFromRepo.Id
};
foreach(var poseId in newInstructorProgramDto.Poses) {
var newPose = new InstructorProgramPose() { PoseId = poseId };
newInstructorProgram.Poses.Add(newPose);
}
_unitOfWork.Repository<InstructorProgram>().Add(newInstructorProgram);
I implemented a solution last night so I'm posting it here.
I added an extension method to the mapping shown below
CreateMap<InstructorProgramForUpdateDto, InstructorProgram>()
.ForMember(d => d.Poses, opt => opt.MapFrom(s => s.Poses.GetInstructorProgramPoses<InstructorProgramPose>()));
Here is the method in the extension class
public static ICollection<T> GetInstructorProgramPoses<T>(this ICollection<int> poses) where T : IInstructorProgramPose, new() {
var instructorProgramPoseColllection = new List<T>();
foreach(var poseId in poses) {
T pose = new T();
pose.PoseId = poseId;
instructorProgramPoseColllection.Add(pose);
}
return instructorProgramPoseColllection;
}

Map Model with Collection Inside

I have an API endpoint something like this:
[HttpGet("{shoppingCartId}")]
public async Task<IActionResult> GetShoppingCart(int shoppingCartId)
{
var shoppingCart = await context.ShoppingCarts.Include(c => c.ShoppingCartItems).SingleOrDefaultAsync(c => c.Id == shoppingCartId);
if (shoppingCart == null)
return NotFound("There is no shoppingCart for specified query.");
return Ok(shoppingCart);
}
It works fine. Returns a ShoppingCart with ShoppingCartItems as expected.
But, I dont want to return shoppingCart but shoppingCartResource.
Something like that:
[HttpGet("{shoppingCartId}")]
public async Task<IActionResult> GetShoppingCart(int shoppingCartId)
{
var shoppingCart = await context.ShoppingCarts.Include(c => c.ShoppingCartItems).SingleOrDefaultAsync(c => c.Id == shoppingCartId);
if (shoppingCart == null)
return NotFound("There is no shoppingCart for specified query.");
var shoppingCartResource = mapper.Map<ShoppingCart, ShoppingCartResource>(shoppingCart);
return Ok(shoppingCartResource);
}
As you can see the ShoppingCart model has a Collection of ShoppingCartItem inside.
public class ShoppingCart
{
public int Id { get; set; }
public DateTime DateCreated { get; set; }
public ICollection<ShoppingCartItem> ShoppingCartItems { get; set; }
public ShoppingCart()
{
ShoppingCartItems = new Collection<ShoppingCartItem>();
}
}
And here is the ShoppingCartResource model
public class ShoppingCartResource
{
public int Id { get; set; }
public DateTime DateCreated { get; set; }
public ICollection<ShoppingCartItemResource> ShoppingCartItemResources { get; set; }
public ShoppingCartResource()
{
ShoppingCartItemResources = new Collection<ShoppingCartItemResource>();
}
}
Mapping Code is:
CreateMap<ShoppingCart, ShoppingCartResource>();
CreateMap<ShoppingCartItem, ShoppingCartItemResource>();
There is no error but I got the only shoppingCartResource with empty ShoppingCartItemResource.
You are lacking single line telling AutoMapper where from it should map ShoppingCartItemResources because corresponding property in source object does not have the same name - ShoppingCartItems, thus it won't work automatically. Just add this:
CreateMap<ShoppingCart, ShoppingCartResource>()
.ForMember(
dst => dst.ShoppingCartItemResources,
opts => opts.MapFrom(src => src.ShoppingCartItems));
CreateMap<ShoppingCartItem, ShoppingCartItemResource>();
By default, AutoMapper follows a naming convention that automatically creates a map between a source property and a target property if their names match, and you don't have to configure anything for this.
But, if you want a map between two properties and their names don't match, then you have to explicitly define a configuration for that map.
Your ShoppingCartResource and ShoppingCart objects both have properties named Id and DateCreated, and so they got mapped automatically.
To get a map between your ICollection properties -
if you want an automatic map, you have to give them a matching name
if you want to keep distinction in naming, you have to explicitly define a configuration for their mapping. Something like -
CreateMap<ShoppingCart, ShoppingCartResource>()
.ForMember(d => d.ShoppingCartItemResources, opt => opt.MapFrom(s => s.ShoppingCartItems));
CreateMap<ShoppingCartItem, ShoppingCartItemResource>();

Linq Includes Alternative

I am a loading a entity from the database like so
var profileEntity = await Context.Profiles
.Include(x => x.MedicalRecords)
.Include(x => x.DrugHistory)
.Include(x => x.EmploymentStatus)
.SingleOrDefaultAsync(x => x.Id == id);
All is working fine, I was just wondering if there is a better way to include its non generic type properties rather using the Include method because this particular entity has a lot of properties I need to include
It is not possible to automatically eagerly load those properties (Mechanism for statically defining eager loading for navigation properties), but you can create a reusable extension method for this purpose:
public static IQueryable<Profile> IncludeAll(this IQueryable<Profile> query)
{
return query.Include(x => x.MedicalRecords)
.Include(x => x.DrugHistory)
.Include(x => x.EmploymentStatus);
}
Which can be used in a following way:
var profileEntity = Context.Profiles.IncludeAll().SingleOrDefault(x => x.Id == id);
One option is to consider turning off lazy loading for those particular navigation properties by making them not virtual. Example below from the MSDN page.
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public string Tags { get; set; }
public ICollection<Post> Posts { get; set; }
}
See the section titled Turning off lazy loading for specific navigation properties at this link for reference.

Prevent SELECT N+1 issue in Entity Framework query using Joins

I'm trying to query something from an indirectly related entity into a single-purpose view model. Here's a repro of my entities:
public class Team {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<Member> Members { get; set; }
}
public class Member {
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
public class Pet {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public Member Member { get; set; }
}
Each class is in a DbSet<T> in my database context.
This is the view model I want to construct from a query:
public class PetViewModel {
public string Name { get; set; }
public string TeamItIndirectlyBelongsTo { get; set; }
}
I do so with this query:
public PetViewModel[] QueryPetViewModel_1(string pattern) {
using (var context = new MyDbContext(connectionString)) {
return context.Pets
.Where(p => p.Name.Contains(pattern))
.ToArray()
.Select(p => new PetViewModel {
Name = p.Name,
TeamItIndirectlyBelongsTo = "TODO",
})
.ToArray();
}
}
But obviously there's still a "TODO" in there.
Gotcha: I can not change the entities at this moment, so I can't just include a List<Pet> property or a Team property on Member to help out. I want to fix things inside the query at the moment.
Here's my current solution:
public PetViewModel[] QueryPetViewModel_2(string pattern) {
using (var context = new MyDbContext(connectionString)) {
var petInfos = context.Pets
.Where(p => p.Name.Contains(pattern))
.Join(context.Members,
p => p.Member.Id,
m => m.Id,
(p, m) => new { Pet = p, Member = m }
)
.ToArray();
var result = new List<PetViewModel>();
foreach (var info in petInfos) {
var team = context.Teams
.SingleOrDefault(t => t.Members.Any(m => m.Id == info.Member.Id));
result.Add(new PetViewModel {
Name = info.Pet.Name,
TeamItIndirectlyBelongsTo = team?.Name,
});
}
return result.ToArray();
}
}
However, this has a "SELECT N+1" issue in there.
Is there a way to create just one EF query to get the desired result, without changing the entities?
PS. If you prefer a "plug and play" repro containing the above, see this gist.
You've made the things quite harder by not providing the necessary navigation properties, which as #Evk mentioned in the comments do not affect your database structure, but allow EF to supply the necessary joins when you write something like pet.Member.Team.Name (what you need here).
The additional problem with your model is that you don't have a navigation path neither from Team to Pet nor from Pet to Team since the "joining" entity Member has no navigation properties.
Still it's possible to get the information needed with a single query in some not so intuitive way by using the existing navigation properties and unusual join operator like this:
var result = (
from team in context.Teams
from member in team.Members
join pet in context.Pets on member.Id equals pet.Member.Id
where pet.Name.Contains(pattern)
select new PetViewModel
{
Name = pet.Name,
TeamItIndirectlyBelongsTo = team.Name
}).ToArray();

Categories

Resources