Many to many relationship mapping in EF Core - c#

I have a problem with many to many relationship in EF core.
I have the following model classes:
public class Meal
{
public int Id { get; set; }
[Required]
public int Insulin { get; set; }
public MealType Type { get; set; }
public ICollection<MealFood> MealFoods { get; set; }
public Meal()
{
MealFoods = new Collection<MealFood>();
}
}
public class Food
{
public int Id { get; set; }
[StringLength(255)]
public string Name { get; set; }
[Required]
public int Carbohydrates { get; set; }
public ICollection<MealFood> MealFoods { get; set; }
public Food()
{
MealFoods = new Collection<MealFood>();
}
}
public class MealFood
{
public int MealId { get; set; }
public Meal Meal { get; set; }
public int FoodId { get; set; }
public Food Food { get; set; }
}
I have the following API resource class:
public class MealResource
{
public int Id { get; set; }
public int Insulin { get; set; }
public MealType Type { get; set; }
public ICollection<FoodResource> Foods { get; set; }
public MealResource()
{
Foods = new Collection<FoodResource>();
}
}
I have done the mapping in my DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MealFood>().HasKey(mf => new { mf.MealId, mf.FoodId });
modelBuilder.Entity<MealFood>().HasOne(mf => mf.Meal).WithMany(m => m.MealFoods).HasForeignKey(mf => mf.MealId);
modelBuilder.Entity<MealFood>().HasOne(mf => mf.Food).WithMany(f => f.MealFoods).HasForeignKey(mf => mf.FoodId);
}
I've got a problem with this call:
var meals = await context.Meals.Include(m => m.MealFoods).ToListAsync();
This returns almost everything I need, except the navigation properties from MealFoods
The reason why I want those properties, because I want to do the following mapping:
CreateMap<Meal, MealResource>().ForMember(mr => mr.Foods, opt => opt.MapFrom(x => x.MealFoods.Select(y => y.Food).ToList()));
I have already found this:
Automapper many to many mapping
but (maybe I don't get something) this doesn't work because the property called Food in MealFood is null.
I hope I didn't explain too complex.

When you include navigation property, EF Core automatically fills the inverse navigation property, e.g. including Meal.MealFoods will automatically fill MealFood.Meal, including Food.MealFoods will automatically populate MealFood.Food etc. In order to populate other navigation properties you need to use additional ThenInclude. E.g.
var meals = await context.Meals
.Include(m => m.MealFoods)
.ThenInclude(mf => mf.Food) // <--
.ToListAsync();
or
var foods = await context.Foods
.Include(f => f.MealFoods)
.ThenInclude(mf => mf.Meal) // <--
.ToListAsync();

Related

Many to Many, self join - how to do this, code first in Fluent Api, EF Core 6?

https://i.imgur.com/rvWQVQt.png
So basically, I want to be able to define a User and have them be able to have a list of other Users that I designate as their friends - for some reason I'm stumped
Here are my classes and attempt so far:
public class User : BaseEntity, IUser
{
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public Guid PhotoId { get; set; }
public string Mobile { get; set; }
public IList<ClubbrEvent> ClubbrEvents { get; set; }
public bool ProfileComplete { get; set; }
public List<UserFriends> Friends { get; set; }
public List<UserFriends> FriendsOf { get; set; }
}
public class UserFriends
{
public Long UserId { get; set; }
public User User { get; set; }
public Long FriendId { get; set; }
public User Friend { get; set; }
}
public class UserFriendsConfiguration: IEntityTypeConfiguration<UserFriends>
{
public void Configure(EntityTypeBuilder<UserFriends> builder)
{
builder.HasOne(f => f.Friend)
.WithMany(fo => fo.FriendsOf)
.HasForeignKey(fk => fk.FriendId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(u => u.User)
.WithMany(f => f.Friends)
.HasForeignKey(fk => fk.UserId);
}
}
But when I try to add a migration I get the following error:
The entity type 'UserFriends' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'. For more information on keyless entity types, see https://go.microsoft.com/fwlink/?linkid=2141943.
Ok, I figured it out, so leaving this here for anyone else in the same situation.
First, I had made a mistake in my join table properties - I had made them long but they should have been guid
Second, I defined the key in the config like so:
builder.HasKey(k => new { k.UserId, k.FriendId });
So in full:
public void Configure(EntityTypeBuilder<UserFriends> builder)
{
builder.HasKey(k => new { k.UserId, k.FriendId });
builder.HasOne(f => f.Friend)
.WithMany(fo => fo.FriendsOf)
.HasForeignKey(fk => fk.FriendId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(u => u.User)
.WithMany(f => f.Friends)
.HasForeignKey(fk => fk.UserId);
}
Running the migration and update now gives me what I need:
https://i.imgur.com/my674wx.png

entity framework core nested object with self parentId

I have a treetable structure and this data comes to me from the frontend.
In this treetable structure, there is IssueActivity and IssueActivityDetail for details of this issue.
Now my question is, more than one IssueActivityDetail field can be added to this IssueActivity field. How can I do this on the c# ef core side?
I tried to do it with the logic of ParentId. My Entity structures are as follows. I did not add the parentId in FluenApi because I did not fully understand it.
My IssueActivity table.
public partial class IssueActivitiy
{
public int Id { get; set; }
public int IssueId { get; set; }
public byte Type { get; set; }
public short SubActivityNo { get; set; }
public string SubActivityTitle { get; set; }
public virtual Issue Issue { get; set; }
public virtual List<IssueActivitiyDetail> IssueActivitiyDetails { get; set; }
}
My IssueActivityDetail table.
public partial class IssueActivitiyDetail
{
public int Id { get; set; }
public int IssueActivityId { get; set; }
public short LineNo { get; set; }
public string Definition { get; set; }
public byte RoleId { get; set; }
public byte Medium { get; set; }
public string Explanation { get; set; }
public int? ParentId { get; set; }
public virtual IssueActivitiy IssueActivity { get; set; }
}
FluentApi Configuration.
public void Configure(EntityTypeBuilder<IssueActivitiy> modelBuilder)
{
modelBuilder.ToTable("IssueActivitiy");
modelBuilder.HasKey(a => a.Id);
modelBuilder.Property(e => e.SubActivityNo).HasComment("Sıra No");
modelBuilder.Property(e => e.SubActivityTitle).HasMaxLength(256).IsUnicode(false);
modelBuilder.Property(e => e.Type).HasDefaultValueSql("((1))").HasComment("1) Temel Aktivite\r\n2) Alternatif Aktivite\r\n3) İşlem İptal Aktivite");
modelBuilder.HasOne(d => d.Issue).WithMany(p => p.IssueActivitiys).HasForeignKey(d => d.IssueId).OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_Issue_IssueActivitiy_Id");
}
public void Configure(EntityTypeBuilder<IssueActivitiyDetail> modelBuilder)
{
modelBuilder.ToTable("IssueActivitiyDetail");
modelBuilder.Property(e => e.Definition).IsRequired().HasMaxLength(2048).IsUnicode(false).HasComment("Açıklama");
modelBuilder.Property(e => e.Explanation).HasMaxLength(2048).IsUnicode(false).HasComment("Açıklama");
modelBuilder.Property(e => e.IssueActivityId).HasComment("Konu Id");
modelBuilder.Property(e => e.LineNo).HasComment("Sıra No");
modelBuilder.Property(e => e.Medium).HasComment("Ortam (Excel, Mail vb.)");
modelBuilder.Property(e => e.RoleId).HasComment("Rol");
modelBuilder.Property(e => e.ParentId);
modelBuilder.HasOne(d => d.IssueActivity).WithMany(p => p.IssueActivitiyDetails).HasForeignKey(d => d.IssueActivityId).OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_IssueActivitiy_IssueActivitiyDetail_");
}
Web Api is also the place where I try to receive and process the data, but I played a lot and couldn't do it correctly.
var vIssueActivity = issueInfo.IssueActivitiyInfos
.Select(a => new IssueActivitiy
{
Type = a.Type,
SubActivityNo = a.SubActivityNo,
SubActivityTitle = a.SubActivityTitle,
IssueActivitiyDetails = a.IssueActivitiyDetailInfos
.Select(x => new IssueActivitiyDetail
{
LineNo = x.LineNo,
Definition = x.Definition,
RoleId = vUser.RoleId,
Medium = x.Medium,
Explanation = x.Explanation,
IssueActivityDetail = new List<IssueActivitiyDetail> { }
}).ToList()
});
You don't need to keep ParentId property in IssueActivityDetail.
public partial class IssueActivitiy
{
...
public virtual List<IssueActivitiyDetail> IssueActivitiyDetails { get; set; }
}
public partial class IssueActivitiyDetail
{
...
public virtual IssueActivitiy IssueActivity { get; set; }
}
Your configuration looks not wrong.
Maybe you can use Include when getting the entity from db context.
var issueActivity = context.IssueActivities.Include(x => x.IssueActivityDetails).FirstOrDefault();
You can accomplish this by retrieving all the entries from the database. Then select the Root node and then let EF Core mapping do the rest.
public class TreeNode
{
public bool IsRoot { get; set; }
public int? ParentNodeId {get; set;}
public virtual List<TreeNode> ChildNodes {get; set;}
}
public class TreeNodeRepository
{
public async Task<TreeNode> GetTreeStructure()
{
var allNodes = await _context.TreeNodes.ToListAsync();
return allNodes.FirstOrDefault(t => t.IsRoot);
}
}
You could argue that ParentId == null would also imply that it's a parent node. this just makes the example given more tuitive imo.
You should consider performance, how many nodes will become an issue, is it exposed through a web-api and would iterating over the nodes be more efficient. So you wouldn't have to load the entire Tree into memory each time but let clients handle that instead.

EF Core FindAsync returns only root object

I have a many to many relation and I am using ASP.Net Core2.2 MVC with EF.
From what I read I understand that at the current time I have to create a CLR class for join table.
My code:
Models:
public class Post
{
public Post()
{
Categories= new List<PostCategory>();
}
public int Id { get; set; }
[Required]
public string Title { get; set; }
[Required]
public string Description { get; set; }
public List<PostCategory> Categories { get; set; }
}
public class Category
{
public Category()
{
Posts= new List<PostCategory>();
}
public int Id { get; set; }
[Required]
public string Name { get; set; }
public ICollection<PostCategory> Posts { get; set; }
}
public class PostCategory
{
public int PostId { get; set; }
public Post Post { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
DBContext:
public class MyDBContext:DbContext
{
public MyDBContext()
{
}
public MyDBContext(DbContextOptions<MyDBContext> options) : base(options)
{
}
public DbSet<Post> Post { get; set; }
public DbSet<Category> Category { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PostCategory>()
.HasKey(x => new { x.PostId, x.CategoryId });
modelBuilder.Entity<PostCategory>()
.HasOne(x => x.Post)
.WithMany(m => m.Categories)
.HasForeignKey(x => x.PostId);
modelBuilder.Entity<PostCategory>()
.HasOne(x => x.Category)
.WithMany(e => e.Posts)
.HasForeignKey(x => x.CategoryId);
}
}
Create Post Code:
var categories= new List<PostCategory>();
foreach (var category in model.SelectedCategories)
{
categories.Add(new PostCategory()
{
CategoryId= category
});
}
model.Post.Categories.AddRange(categories);
_context.Post.Add(model.Post);
await _context.SaveChangesAsync();
When I create a post I can see in database that I have Post data, and in PostCategory table I can see PostId CategoryId as they should be.
My issue is when I try to get a post data using:
var post = await _context.Post.FindAsync(id);
post.Categories count is always 0, what am I missing?
You need to write your query as follows to eager load the related Categories with Post:
var post = await _context.Post.Include(p => p.Categories).FirstOrDefaultAsync(p => p.Id == id);
Here is the more details about Loading Related Data in EF core
The accepted answer will work but may not be as performant - when using FindAsync if the entity is already being tracked you'll get that copy rather than going to the database
In order to have that and load related data you need to do it manually
for example:
context.Entry(post)
.Reference(p => p.Categories)
.Load();
references:
https://learn.microsoft.com/en-us/ef/core/querying/related-data/explicit#explicit-loading
https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.findasync?view=efcore-7.0#overloads

Map components of DTO which are DTOs as well

This is my class which holds database data:
public partial class PermissionGroup
{
public int Id { get; set; }
public string Name { get; set; }
// other database properties
public virtual ICollection<GroupActionPermission> GroupActionPermissions { get; set; }
}
And that's my dto's:
public class PermissionGroupDTO
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<GroupActionPermissionDTO> ActionPermissions { get; set; }
}
public class GroupActionPermissionDTO
{
public int Id { get; set; }
public int GroupId { get; set; }
public int PermissionActionId { get; set; }
public PermissionGroupDTO Group { get; set; }
}
Now, I am making mapping:
public IEnumerable<PermissionGroupDTO> GetGroups()
{
return OnConnect<IEnumerable<PermissionGroupDTO>>(db =>
{
return db.PermissionGroups
.Include(i => i.GroupActionPermissions)
.ProjectTo<PermissionGroupDTO>()
.ToList();
});
}
And I am getting collection of PermissionGroupDTO which should contains collection of GroupActionPermissionDTO, but that collection stays null. Is there something wrong with my code? I am afraid that automapper can map collections from foreign keys.
Also, thats my automapper initializer:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<PermissionGroup, PermissionGroupDTO>();
cfg.CreateMap<GroupActionPermission, GroupActionPermissionDTO>();
});
I believe the reason is desribed here http://docs.automapper.org/en/stable/Queryable-Extensions.html
Note that for this feature to work, all type conversions must be explicitly handled in your Mapping.
So that means you should manually configure the mapping:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<PermissionGroup, PermissionGroupDTO>()
.ForMember(dto => dto.ActionPermissions , conf => conf.MapFrom(ol => ol.GroupActionPermissions )));;
cfg.CreateMap<GroupActionPermission, GroupActionPermissionDTO>();
});
BTW, note that fields are named differently: GroupActionPermissions vs. ActionPermissions. This is also the reason why automapper doesn't map it automatically and then you should use the manual configuration I wrote.

Why is The navigation property '' declared on type '' has been configured with conflicting multiplicities. error show up?

I have an Asp_Users table and a Resource tables. In the Entity Framework world, I have a Asp_Users POCO and a Resource POCO. Moreover, the Resource POCO is abstract and is part of a Table-per-Hierarchy model. The Table-per-Hierarchy model has the abstract Resource POCO and several Concrete POCOs like ILCResource POCO and SectionResource POCO. There is a one-to-many (1 to 0…*) relationship from Asp_Users POCO (one-side) to Resource POCO (many-side).
Here's the relevant part of my aspnet_Users POCO:
public partial class aspnet_Users
{
public aspnet_Users() { }
public virtual System.Guid ApplicationId { get; set; }
public virtual System.Guid UserId { get; set; }
public virtual string UserName { get; set; }
public virtual string LoweredUserName { get; set; }
public virtual string MobileAlias { get; set; }
public virtual bool IsAnonymous { get; set; }
public virtual System.DateTime LastActivityDate { get; set; }
public virtual ICollection<Resource> associatedResources { get; set; }
}
Here is my mapping configuration for Asp_Users
public class Aspnet_UsersMap : EntityTypeConfiguration<PerlsData.Domain.aspnet_Users>
{
public Aspnet_UsersMap()
{
this.ToTable("aspnet_Users", schemaName: "dbo");
this.HasKey(u => u.UserId);
this.Property(u => u.UserId)
.HasColumnName("UserId")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.HasOptional(u => u.associatedResources);
}
}
Here's the relevant part of my abstract Resource POCO class:
public abstract class Resource
{
public Resource(){
// associatedPerspectives = new HashSet<Perspective>();
}
public virtual System.Guid ResourceDatabaseID { get; set; }
public virtual string ResourceName { get; set; }
public virtual string DescriptionOfResource { get; set; }
public virtual System.Guid UserId { get; set; }
public virtual Nullable<System.Guid> DepartmentDatabaseID { get; set; }
public virtual string ResourceStatus { get; set; }
public virtual Nullable<short> isRemoved { get; set; }
public virtual Department Department { get; set; }
public virtual System.Guid UserId { get; set; }
public virtual aspnet_Users aspnet_Users { get; set; }
public virtual ICollection<ResourceOverToILCResourcesBridge> associatedResourceOverToILCResourcesBridgeEntry { get; set; }
}
Here is my mapping configuration for Resource:
public class ResourceMap : EntityTypeConfiguration<Resource>
{
public ResourceMap()
{
this.ToTable("Resources", schemaName: "dbo");
this.Property(r => r.ResourceDatabaseID)
.HasColumnName("ResourceDatabaseID");
this.HasKey(r => r.ResourceDatabaseID);
this.Property(x => x.ResourceDatabaseID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
// .StoreGeneratedPattern = StoreGeneratedPattern.Identity;
this.Property(r => r.ResourceName)
.HasColumnName("ResourceName");
this.Map<PerlsData.Domain.OtherItem>(m => m.Requires("discriminator").HasValue("otheritems"))
.Map<PerlsData.Domain.Audioitem>(m => m.Requires("discriminator").HasValue("audioitems"))
.Map<PerlsData.Domain.Imageitem>(m => m.Requires("discriminator").HasValue("imageitems"))
.Map<PerlsData.Domain.Videoitem>(m => m.Requires("discriminator").HasValue("videoitems"))
.Map<PerlsData.Domain.UriItem>(m => m.Requires("discriminator").HasValue("uriitems"))
.Map<PerlsData.Domain.Documentitem>(m => m.Requires("discriminator").HasValue("documentitems"))
.Map<PerlsData.Domain.DatabaseFileItem>(m => m.Requires("discriminator").HasValue("databasefileitems"));
this.HasOptional(res => res.associatedResourceOverToILCResourcesBridgeEntry);
this.HasRequired(res => res.aspnet_Users)
.WithMany(u => u.associatedResources)
.HasForeignKey(res => res.UserId);
}
}
Could you please tell me why I am getting the following error?
The navigation property 'associatedResources' declared on type
'PerlsData.Domain.aspnet_Users' has been configured with conflicting
multiplicities.
Please Explain Why it's still NULL after I created the mapping in the POCO class.
this.HasOptional(u => u.associatedResources);
That's wrong. HasOptional means 0..1.
You want HasMany().
In fact, you can get rid of that line entirely; EF can figure it out from the property.
I need to study more about the IQueryable and/or IEnumerable collections along with the lambda expression queries that are used to retrieve data, and place them in the aforementioned IQueryable and/or IEnumerable.
My problem was that I was trying to retrieve the associations of a particular POCO within a loop that iterates over the IQueryable and/or IEnumerable collections.
Here is the Faulty code (we are accessing association while we iterate over IQueryable):
resourcestempIQueryable = context.Resources;
foreach (PerlsData.Domain.Resource tempRes in resourcestempIQueryable)
{
string[] resourceRow = { tempRes.ResourceName,
tempRes.DescriptionOfResource,
tempRes.UploadDate.ToString(),
tempRes.Iconpath,
tempRes.aspnet_Users.UserName, // Error
tempRes.ResourceDatabaseID.ToString() };
tempDataTableForResources.Rows.Add(resourceRow);
}
Here is the proper code that should resolve the issue:
using (PerlsData.Context context = new PerlsData.Context())
{
context.Configuration.LazyLoadingEnabled = true;
context.Configuration.ProxyCreationEnabled = true;
resourcesIEnumerable = context.Resources.ToList<PerlsData.Domain.Resource>();
var entityValuesWithFieldsToShowInAllResourcesPage = context.Resources.Select(res => new { res.ResourceName, res.DescriptionOfResource, res.UploadDate, res.Iconpath, res.aspnet_Users.UserName, res.ResourceDatabaseID });
foreach (var varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage in entityValuesWithFieldsToShowInAllResourcesPage)
{
string[] resourceRow = {
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.ResourceName,
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.DescriptionOfResource,
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.UploadDate.ToString(),
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.Iconpath,
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.UserName,
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.ResourceDatabaseID.ToString()
};
tempDataTableForResources.Rows.Add(resourceRow);
}
}

Categories