How to eagerly load all referenced entities dynamically in Entity Framework - c#

EF 6
I have the following POCO's
_
public class StructureEntity : EmEntityBase
{
[ForeignKey("ParentStructureId")]
public virtual StructureEntity ParentStructure { get; set; }
public long? ParentStructureId { get; set; }
[ForeignKey("SiteId")]
public virtual SiteEntity Site { get; set; }
[Required(ErrorMessage = "Required.")]
public long SiteId { get; set; }
[Required(ErrorMessage = "Required.")]
public string Name { get; set; }
}
public class SiteEntity : EmEntityBase
{
[ForeignKey("ParentSiteId")]
public virtual SiteEntity ParentSite { get; set; }
public long? ParentSiteId { get; set; }
[Required(ErrorMessage = "Required.")]
public long ClientId { get; set; }
[ForeignKey("ClientId")]
public ClientEntity Client { get; set; }
[Required(ErrorMessage = "Required.")]
public string Name { get; set; }
}
public class ClientEntity : EmEntityBase
{
public long? ParentClientId { get; set; }
[ForeignKey("ParentClientId")]
public virtual ClientEntity ParentClient { get; set; }
public string Name { get; set; }
}
Now when I want to eagerly load all the referenced entities. To do this I have:
public IQueryable<StructureDivisionEntity> GetAllWithInclude()
{
return GetAll()
.Include(e => e.Structure)
.Include(e => e.ParentStructureDivision.Structure)
.Include(e => e.Structure.Site.Client);
}
I was wondering if there was a dynamic way to do this without explicitly having to do the .Include(Structure) etc. Something along the lines of:
MyEntity.IncludeAllReferenced() where IncludeAllReferenced used reflection or similair to traverse MyEntity and do all the includes?
UPDATE: Alternative Approach To Querying Complex Graphs
http://www.codeproject.com/Articles/247254/Improving-Entity-Framework-Query-Performance-Using
https://entityframework.codeplex.com/workitem/1386
https://github.com/oinuar/GBQ

You can't do that, but you can make an extensionmethod to get all.
This might be a crazy idea, but you could make a generic extension-method to handle all your types, to make the code more clean.
Example, (one could extend this with factory-pattern if necessary):
public static IQueryable<T> IncludeAll<T>(this IQueryable<T> query)
{
if (typeof(T) == typeof(StructureDivisionEntity))
{
return GetAllWithInclude(query);
}
throw new NotImplementedException("IncludeAll not implemented for type {0}",typeof(T).FullName);
}
private static IQueryable<StructureDivisionEntity> GetAllWithInclude<StructureDivisionEntity> (IQueryable<StructureDivisionEntity> query)
{
return query.Include(e => e.Structure)
.Include(e => e.ParentStructureDivision.Structure)
.Include(e => e.Structure.Site.Client);
}

Related

C# include relationship object from many to many with automapper

I'm trying to map from Role to RoleModel including the Feature/FeatureModel, but I haven't been able to make it work properly.
It returns the role but does not include the features:
{
"name": "Backend",
"description": "Backend role",
"roleFeatures": [],
"roleUsers": [],
"updatedDate": "2023-02-03T16:14:54.036441",
"deletedDate": null,
"id": "82a443bd-81d3-4460-8a67-08db0601365e",
"createdDate": "2023-02-03T16:14:54.036441",
"deleted": false
}
This is the map I created:
CreateMap<Role, RoleModel>()
.ForMember(rm => rm.RoleFeatures, opt => opt
.MapFrom(r => r.RoleFeatures.Select(y => y.Feature).ToList()))
.MaxDepth(1);
These are the classes:
Role
public class Role : BaseRecord
{
public string Name { get; set; }
public string Description { get; set; }
public ICollection<RoleFeature> RoleFeatures { get; set; }
public ICollection<RoleUser> RoleUsers { get; set; }
}
RoleModel
public class RoleModel : BaseRecordModel
{
public string Name { get; set; }
public string Description { get; set; }
public ICollection<RoleFeatureModel> RoleFeatures { get; set; }
public ICollection<RoleUserModel> RoleUsers { get; set; }
}
RoleFeature
public class RoleFeature
{
public bool Read { get; set; }
public bool Write { get; set; }
public bool Edit { get; set; }
public bool Delete { get; set; }
public Guid RoleId { get; set; }
public Role Role { get; set; }
public Guid FeatureId { get; set; }
public Feature Feature { get; set; }
}
RoleFeatureModel
public class RoleFeatureModel
{
public Guid RoleId { get; set; }
public Guid FeatureId { get; set; }
public bool Read { get; set; }
public bool Write { get; set; }
public bool Edit { get; set; }
public bool Delete { get; set; }
public RoleModel Role { get; set; }
public FeatureModel Feature { get; set; }
}
Previously I was creating a simple map:
CreateMap<Role, RoleModel>();
But I was getting an object cycle error, now I get the roleModel but the roleFeature list is empty.
I'm gonna listen to what my guts tell me - you are querying for the roles in lazy loading manner (default), aren't you?
You have to include related members explicitly, for example:
var resultSet = await _context.Roles.Include(r => r.RoleFeatures).ToListAsync();
Include() method enforces so called eager loading, providing you not only with requested data, but also its related members.
It also can be chained e.g.:
var resultSet = _context.Roles.Include(r => r.RoleFeatures)
.Include(r => r.RoleUsers)
.ToListAsync();
OR
If you are already using eager loading.
Instead of constraining your mapper with MaxDepth(1), go to your ConfigureServices() (in your Startup.cs or Program.cs) method, and configure JsonSerializer in a way that suits you the best, for example:
services.AddNewtonsoftJson(options =>
{
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
options.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize;
options.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.All;
});
Above example uses Newtonsoft.Json, but you can achieve the same effect reffering ReferenceLoopHandling with System.Text.Json serializer.

EF include list is always null

By some reason EF wont load the included list properly so it ends up being null all the time.
Here is the entities i'm using:
[Table("searchprofilepush")]
public class SearchProfilePush
{
public int Id { get; set; }
public int AccountId { get; set; }
public bool Push { get; set; }
public int UserPushId { get; set; }
public UserPush UserPush { get; set; }
public int SearchProfileId { get; set; }
public SearchProfile SearchProfile { get; set; }
public ICollection<SearchProfileMediaTypePush> SearchProfileMediaTypePush { get; set; }
}
[Table("searchprofilemediatypepush")]
public class SearchProfileMediaTypePush
{
public int Id { get; set; }
public MediaTypeType MediaType { get; set; }
public bool Push { get; set; }
public int SearchProfilePushId { get; set; }
public SearchProfilePush SearchProfilePush { get; set; }
}
Then when i'm trying to do this:
var searchProfilePush = _dataContext.SearchProfilePush.Include(w => w.SearchProfileMediaTypePush).FirstOrDefault(w => w.AccountId == accountId && w.SearchProfileId == searchProfileId);
My included list is always null.
I guess it's some obvious reason why this doesn't work but i just can't figure it out.
Thanks!
EDIT:
Here is the sql query:
SELECT \"Extent1\".\"id\", \"Extent1\".\"accountid\", \"Extent1\".\"push\", \"Extent1\".\"userpushid\", \"Extent1\".\"searchprofileid\" FROM \"public\".\"searchprofilepush\" AS \"Extent1\" WHERE \"Extent1\".\"accountid\" = #p__linq__0 AND #p__linq__0 IS NOT NULL AND (\"Extent1\".\"searchprofileid\" = #p__linq__1 AND #p__linq__1 IS NOT NULL) LIMIT 1
EDIT 2:
I have now mapped my entities both way and the list is still always null.
Edit 3:
This is how i created my database tables.
The documentation I read for loading related entities has some differences with the sample code and your code. https://msdn.microsoft.com/en-us/library/jj574232(v=vs.113).aspx
First, when you define your ICollection, there is no keyword virtual:
public virtual ICollection<SearchProfileMediaTypePush> SearchProfileMediaTypePush { get; set; }
Next, in the example close to yours, where they load related items using a query, the first or default is not using a boolean expression. The selective expression is in a where clause:
// Load one blogs and its related posts
var blog1 = context.Blogs
.Where(b => b.Name == "ADO.NET Blog")
.Include(b => b.Posts)
.FirstOrDefault();
So you can try:
var searchProfilePush = _dataContext.SearchProfilePush
.Where(w => w.AccountId == accountId && w.SearchProfileId == searchProfileId)
.Include(w => w.SearchProfileMediaTypePush)
.FirstOrDefault();
Can you make these two changes and try again?
A few things will be an issue here. You have no keys defined or FKs for the relationship:
[Table("searchprofilepush")]
public class SearchProfilePush
{
[Key]
public int Id { get; set; }
public int AccountId { get; set; }
public bool Push { get; set; }
public int UserPushId { get; set; }
public UserPush UserPush { get; set; }
public int SearchProfileId { get; set; }
public SearchProfile SearchProfile { get; set; }
public ICollection<SearchProfileMediaTypePush> SearchProfileMediaTypePush { get; set; }
}
[Table("searchprofilemediatypepush")]
public class SearchProfileMediaTypePush
{
[Key]
public int Id { get; set; }
public MediaTypeType MediaType { get; set; }
public bool Push { get; set; }
public int SearchProfilePushId { get; set; }
[ForeignKey("SearchProfilePushId")]
public SearchProfilePush SearchProfilePush { get; set; }
}
Personally I prefer to explicitly map out the relationships using EntityTypeConfiguration classes, but alternatively they can be set up in the Context's OnModelCreating. As a starting point have a look at http://www.entityframeworktutorial.net/code-first/configure-one-to-many-relationship-in-code-first.aspx for basic EF relationship configuration.
for a SearchProfilePush configuration:
modelBuilder.Entity<SearchProfilePush>()
.HasMany(x => x.SearchProfileMediaTypePush)
.WithRequired(x => x.SearchProfilePush)
.HasForeignKey(x => x.SearchProfilePushId);

AutoMapper with different children

I have an entity as Plan with multiple sub-plans (children), each of which could be null.
For the PlanDto, I am trying to load up a list of all children rather than having a separate property for each child like the entity.
I have already achieved it manually through a foreach loop but now I am trying to do it via AutoMapper, which is failing for some reason.
Entities:
public class Plan
{
public virtual int Id { get; set; }
public DateTime Date { get; set; }
public virtual PlanDetail PlanChild1 { get; set; }
public virtual ObservationCare PlanChild2 { get; set; }
}
public class PlanDetail
{
public virtual int Id { get; set; }
public virtual Plan Plan { get; set; }
public virtual string Description { get; set; }
}
public class ObservationCare
{
public virtual int Id { get; set; }
public virtual Plan Plan { get; set; }
public virtual string Description { get; set; }
}
DTOs:
public class PlanDto: EntityDto
{
public DateTime Date { get; set; }
public IEnumerable<ChildPlan> ChildPlan { get; set; }
}
public class ChildPlan : EntityDto
{
public ChildPlanType Type { get; set; }
}
public enum ChildPlanType
{
PlanDetail,
ObservationCare
}
AutoMapper config:
configuration.CreateMap<Plan, PlanDto>();
configuration.CreateMap<PlanDetail, ChildPlan>()
.ForMember(dto => dto.Type, options => options.MapFrom(p => ChildPlanType.PlanDetail));
configuration.CreateMap<ObservationCare, ChildPlan>()
.ForMember(dto => dto.Type, options => options.MapFrom(p => ChildPlanType.ObservationCare));
Mapping attempt:
var output = new List<PlanDto>();
var plans = await _planRepository.GetAll().ToList();
foreach (var plan in plans)
{
output.Add(ObjectMapper.Map<PlanDto>(plan));
}
I do not know why ChildPlan DTOs in the output list are always null!
You have to specify the mapping for PlanDto.ChildPlan:
configuration.CreateMap<Plan, PlanDto>()
.ForMember(dto => dto.ChildPlan,
options => options.MapFrom(
p => new object[] { p.PlanChild1, p.PlanChild2 }.Where(c => c != null)));
If you are using Entity Framework Core, you have to use eager-loading:
var plans = await _planRepository.GetAll()
.Include(p => p.PlanChild1)
.Include(p => p.PlanChild2)
.ToList();
There's also a simpler and more efficient way to map a list:
var output = ObjectMapper.Map<List<PlanDto>>(plans);

AutoMapper throwing StackOverflowException when calling ProjectTo<T>() on IQueryable

I have created classes using EF Code First that have collections of each other.
Entities:
public class Field
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<AppUser> Teachers { get; set; }
public Field()
{
Teachers = new List<AppUser>();
}
}
public class AppUser
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
public virtual List<Field> Fields { get; set; }
public AppUser()
{
Fields = new List<FieldDTO>();
}
}
DTOs:
public class FieldDTO
{
public int Id { get; set; }
public string Name { get; set; }
public List<AppUserDTO> Teachers { get; set; }
public FieldDTO()
{
Teachers = new List<AppUserDTO>();
}
}
public class AppUserDTO
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
public List<FieldDTO> Fields { get; set; }
public AppUserDTO()
{
Fields = new List<FieldDTO>();
}
}
Mappings:
Mapper.CreateMap<Field, FieldDTO>();
Mapper.CreateMap<FieldDTO, Field>();
Mapper.CreateMap<AppUserDTO, AppUser>();
Mapper.CreateMap<AppUser, AppUserDTO>();
And I am getting StackOverflowException when calling this code (Context is my dbContext):
protected override IQueryable<FieldDTO> GetQueryable()
{
IQueryable<Field> query = Context.Fields;
return query.ProjectTo<FieldDTO>();//exception thrown here
}
I guess this happens because it loops in Lists calling each other endlessly. But I do not understand why this happens. Are my mappings wrong?
You have self-referencing entities AND self-referencing DTOs. Generally speaking self-referencing DTOs are a bad idea. Especially when doing a projection - EF does not know how to join together and join together and join together a hierarchy of items.
You have two choices.
First, you can force a specific depth of hierarchy by explicitly modeling your DTOs with a hierarchy in mind:
public class FieldDTO
{
public int Id { get; set; }
public string Name { get; set; }
public List<TeacherDTO> Teachers { get; set; }
public FieldDTO()
{
Teachers = new List<TeacherDTO>();
}
}
public class TeacherDTO
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
}
public class AppUserDTO : TeacherDTO
{
public List<FieldDTO> Fields { get; set; }
public AppUserDTO()
{
Fields = new List<FieldDTO>();
}
}
This is the preferred way, as it's the most obvious and explicit.
The less obvious, less explicit way is to configure AutoMapper to have a maximum depth it will go to traverse hierarchical relationships:
CreateMap<AppUser, AppUserDTO>().MaxDepth(3);
I prefer to go #1 because it's the most easily understood, but #2 works as well.
Other option is using PreserveReferences() method.
CreateMap<AppUser, AppUserDTO>().PreserveReferences();
I use this generic method:
public static TTarget Convert<TSource, TTarget>(TSource sourceItem)
{
if (null == sourceItem)
{
return default(TTarget);
}
var deserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace, ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
var serializedObject = JsonConvert.SerializeObject(sourceItem, deserializeSettings);
return JsonConvert.DeserializeObject<TTarget>(serializedObject);
}
...
MapperConfiguration(cfg =>
{
cfg.ForAllMaps((map, exp) => exp.MaxDepth(1));
...
When you giving 1 navigation_property to 2nd entity and visa-versa it go in an infinite loop state. So, the compiler automatically throws a Stackoverflow exception.
So, to avoid that, you just need to remove one navigation_property from any of the entities.

A better way to retrieve EF data and collections

So I have a model that contains a list of models which contains items, and so on, like this:
public partial class CART
{
public CART()
{
//this.CART_DETAIL = new HashSet<CART_DETAIL>();
this.CART_DETAIL = new List<CART_DETAIL>();
}
public int CART_IDE { get; set; }
public int CART_COUNT { get; set; }
public string SHOPPING_CART_IDE { get; set; }
public virtual IList<CART_DETAIL> CART_DETAIL { get; set; }
}
public partial class CART_DETAIL
{
public int CART_DETAIL_IDE { get; set; }
public int CART_IDE { get; set; }
public int CART_DETAIL_COUNT { get; set; }
public Nullable<int> PACK_IDE { get; set; }
public Nullable<int> BACKSTORE_INVENTORY_IDE { get; set; }
public virtual CART CART { get; set; }
public virtual PACK PACK { get; set; }
public virtual BACKSTORE_INVENTORY BACKSTORE_INVENTORY { get; set; }
}
public partial class BACKSTORE_INVENTORY
{
public BACKSTORE_INVENTORY()
{
this.CART_DETAIL = new HashSet<CART_DETAIL>();
this.ORDER_DETAIL = new HashSet<ORDER_DETAIL>();
}
public int BACKSTORE_INVENTORY_IDE { get; set; }
public int INVENT_IDE { get; set; }
public int STORE_IDE { get; set; }
public decimal BACKSTORE_INVENTORY_PRICE { get; set; }
public int BACKSTORE_STOCK_QTY { get; set; }
public decimal BACKSTORE_DISCOUNT { get; set; }
public decimal BACKSTORE_SELLING_PRICE { get; set; }
public virtual INVENTORY INVENTORY { get; set; }
public virtual STORE STORE { get; set; }
public virtual ICollection<CART_DETAIL> CART_DETAIL { get; set; }
public virtual ICollection<ORDER_DETAIL> ORDER_DETAIL { get; set; }
}
When I open a connection and consult the data, everything's fine, but if I retrive the whole data in a view, for example, unless I modify the Hashset to a List and then proceed like this:
CART cart =
db.CART.FirstOrDefault(_item => _item.SHOPPING_CART_IDE == mShoppingCartID && _item.CART_ACTIVE_INDICATOR);
if (cart != null)
{
cart.CART_EXP_TIME = DateTime.Now.AddMinutes(90);
cart.USER_SESSION_IDE = UserSessionManager.GetUserSession().mUserSessionID;
cart.CART_DETAIL = cart.CART_DETAIL.ToList();
foreach (var cartDetail in cart.CART_DETAIL)
{
if(cartDetail.BACKSTORE_INVENTORY_IDE != null)
{
cartDetail.BACKSTORE_INVENTORY =
db.BACKSTORE_INVENTORY.First(_item => _item.BACKSTORE_INVENTORY_IDE == cartDetail.BACKSTORE_INVENTORY_IDE);
cartDetail.BACKSTORE_INVENTORY.INVENTORY =
db.INVENTORY.Find(cartDetail.BACKSTORE_INVENTORY.INVENT_IDE);
cartDetail.BACKSTORE_INVENTORY.INVENTORY.CARD =
db.CARD.Find(cartDetail.BACKSTORE_INVENTORY.INVENTORY.CARD_IDE);
}
else
{
cartDetail.PACK = db.PACK.First(_item => _item.PACK_IDE == cartDetail.PACK_IDE);
}
}
db.SaveChanges();
}
I get the following error: CS0021: Cannot apply indexing with [] to an expression of type 'System.Collections.Generic.ICollection<MyApp.Models.DAL.Entities.CART_DETAIL>' which I understand is because the ICollection does not afford indexing, and then I get The ObjectContext instance has been disposed and can no longer be used for operations that require a connection. for items that I forgot to retrive.
So my question: what makes this happen? Is there a way to retrieve all the data at once without having to get all specific items separately? A better way to do things?
What are you trying to achieve form the above code?
I am struggling to follow what your end goal is but would something along these lines be what you are looking for:
public List<Cart> GetAllInCart()
{
return db.CART.Where(a => a.Cart_IDE == CartIDE)
.Include(x => x.Cart_Detail)
.Include(x => x.Cart_Detail.Pack)
.Include(x => x.Cart_Detail.Backstore_Inventory)
.ToList()
}
I hope this helps :)

Categories

Resources