EF access navigation properties in model - c#

I have an entity like below
public class Role
{
[Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required, StringLength(30)]
public string Name { get; set; }
public virtual ICollection<User> Users { get; set; }
public virtual ICollection<RolePermission> Permissions { get; set; }
public bool HasPermission(String code)
{
foreach (var p in this.Permissions)
{
if (p.Permission.Code.Equals(code))
return true;
}
return false;
}
}
and in controller, this code run fine:
for (var p in db.User.Where(u => u.UserId == 1).First().Role.Permissions) { PrintToDebug(); }
but:
User ur = db.User.Where(u => u.UserId == 1).First();
ur.Role.HasPermission("Some_Code_Were_Defined");
then the Permissions list in HasPermission always has a zero length, why and how to solve?

This is occurring because of Entity Framework Lazy Loading. In your first statement, you are specifically requesting the Permissions property, which causes Entity Framework to generate a query that loads that table from the database. In the second query, you are only asking Entity Framework to load the User table from the database, but the HasPermission method you are calling has no way of making another database call to load the Permissions table.
This is a common issue when working with Entity Framework. It can be resolved by using the Include() extension method from Entity Framework to eagerly load the related table in the second query, i.e. User ur = db.User.Where(u => u.UserId == 1).Include(u => u.Role.Permissions).First();

Related

EF Core - how to model relation of Grandparent - Parent - Child on same model

Imagine a model of User that can have Parents and also can have Children.
How would you model such a case in EF Core?
I tried with something like that (pseudo-code)
public class User
{
public ICollection<Relation> Relations {get;set;}
public ICollection<User> Parents => Relation.Where(r => r.Relation == 'Parents')
public ICollection<User> Children => Relation.Where(r => r.Relation == 'Children')
}
public class Relaction
{
public User User1 {get;set;}
public Guid User1Id {get;set;}
public User User2 {get;set;}
public Guid User2Id {get;set;}
public Relation Relation {get;set;} //some enum or sth to indicate relation type
}
But in such modeling, I'm not able to force EF DbContext to fetch into User.Relations data where UserId is in User1Id and in User2Id.
Any idea?
What you are asking for is a classic many-to-many self relationship - (1) user as parent can have many users as children, and (2) user as child can have many users as parents.
Thus it is modelled with one main entity and one join (linking) entity similar to what you have shown. The linking entity does not need special indicator because the two FKs determine the role. i.e. lets change your example with more descriptive names:
public class User
{
public Guid Id { get; set; }
}
public class UserRelation
{
public User Parent { get; set; }
public User Child { get; set; }
public Guid ParentId { get; set; }
public Guid ChildId { get; set; }
}
Now, in pseudo code, given User user, then
user.Parents = db.Users.Where(u => user == u.Child)
user.Children = db.Users.Where(u => user == u.Parent)
EF Core 5.0+ allows you to hide the join entity (it still exists, but is maintained implicitly) and model the relationship with the so called skip navigations, which are the natural OO way of representing such relationship, e.g. the model becomes simply
public class User
{
public Guid Id { get; }
public ICollection<User> Parents { get; set; }
public ICollection<User> Children { get; set; }
}
This is all needed to create such relationship.
However the name of the join table and its columns by convention won't be what normally you would do - in this case, they would be "UserUser" table with "ParentsId" and "ChildrenId" columns.
If you use migrations and don't care about the names, then you are done and can safely skip the rest.
If you do care though, luckily EF Core allows you to change the defaults with fluent configuration (even though in a not so intuitive way):
modelBuilder.Entity<User>()
.HasMany(e => e.Parents)
.WithMany(e => e.Children)
.UsingEntity<Dictionary<string, object>>("UserRelation",
je => je.HasOne<User>().WithMany()
.HasForeignKey("ParentId").IsRequired().OnDelete(DeleteBehavior.Restrict),
je => je.HasOne<User>().WithMany()
.HasForeignKey("ChildId").IsRequired().OnDelete(DeleteBehavior.Restrict),
je => je.ToTable("UserRelations")
.HasKey("ParentId", "ChildId")
);
Here Dictionary<string, object> is the shared type EF Core will use to maintain the join entity in memory (change tracker). And is the most annoying thing in the above configuration since in a future they might change their minds and use different type (there are actually plans to do that in EF Core 6.0), so you'll have to update your mappings. Note that this does not affect the database design, just the memory storage type in EF Core change tracker.
So, because of that and the fact that in some scenarios it is better to work directly with the join entity, you could actually combine both approaches (explicit join entity and skip navigations) and get the best of both worlds.
To do than, you add the explicit entity and (optionally) navigations from/to it. The next is w/o collection navigations from User to UserRelation (with fully defined navigation you would need two ICollection<UserRelation> properties there):
public class User
{
public Guid Id { get; }
public ICollection<User> Parents { get; set; }
public ICollection<User> Children { get; set; }
}
public class UserRelation
{
public User Parent { get; set; }
public User Child { get; set; }
public Guid ParentId { get; set; }
public Guid ChildId { get; set; }
}
and required minimal fluent configuration
modelBuilder.Entity<User>()
.HasMany(e => e.Parents)
.WithMany(e => e.Children)
.UsingEntity<UserRelation>(
je => je.HasOne(e => e.Parent).WithMany(), // <-- here you would specify the corresponding collection nav property when exists
je => je.HasOne(e => e.Child).WithMany(), // <-- here you would specify the corresponding collection nav property when exists
je => je.ToTable("UserRelations")
);
The result is the same database model, but with different in-memory representation of the join entity and ability to query/manipulate it directly. Actually you can do the same with implicit entity, but in type unsafe way using sting names and object values which need to be cast to the appropriate type. This probably will improve in the future if they replace Dictionary<string, object> with some generic type, but for now explicit entity combined with skip navigations looks the best.
You can find (I guess better than mine) explanation of all this in the official EF Core documentation - Many-to-many and the whole Relationships section in general.

EF core navigation property return null value even after using Incude

This is not a duplicate question as I have looked up many questions including this, which is the closest to what I want but didn't solve the challenge.
I have my table models relation set up this way:
public class User
{
public long UserId { get; set; }
public string Name { get; set; }
public IList<Transaction> Transactions { get; set; }
}
public class Transaction
{
public long TransactionId { get; set; }
public User User { get; set; }
public User Patient { get; set; }
}
fluent api setup for the entity
//some other modelbuilder stuff
modelBuilder.Entity<User>(entity =>
{
entity.HasMany(e => e.Transactions).WithOne(e => e.User);
//wanted to add another entity.HasMany(e => e.User).WithOne(e => e.Patient) but efcore didn't allow me.
});
This generates a Transaction table with UserUserId and PatientUserId and takes the right values on save.
But when I do a get with a user Id
User user = dbcontext.Set<User>().Include(t => t.Transactions).FirstOrDefault(u => u.UserId == userId);
user.Transactions have a list of transaction all with null Transaction.Patient
What exactly is going on here and how do I get past it?
Thanks.
You are nesting navigations. So, you have to use ThenInclude like this to add Patient which is a navigation property of Transaction.
User user = dbcontext.Set<User>().Include(t => t.Transactions).ThenInclude(p => p.Patient).FirstOrDefault(u => u.UserId == userId);

Load child entity on the fetch of the Parent entity EFCore

I have the below model. What is the better way to load the parent entity with child entity at the time of fetching from the DB with find method?
Parent Entity:
public class Client
{
public int Id { get; set; }
public string LastName { get; set; }
public string Gender { get; set; }
public DateTime DateOfBirth { get; set; }
public Address Address { get; set; }
}
Child Entity:
public class Address
{
public int Id { get; set; }
public string FirstLine { get; set; }
public string SecondLine { get; set; }
public string Province { get; set; }
}
Now when I try to fetch the data using the Find method I got the address entity null, but when I check in the DB data exist for that ID in Child table too.
referenceContext.Clients.Find(client.Id);
Is there a way to overcome this? When I fetch the parent object and at the same time the value of the child entity is also loaded with the parent.
Notes: As of now, if I used the Include(i => i.Address) then, and then, only I am able to load the child entity.
I already use the Include but is there any other option exist to load child entity if I get the parent entity.
referenceContext.Clients.Where(c => c.IsActive.Equals(true))
.Include(i => i.Address).ToList();
As you said:
Notes: As of now, if I used the Include(i => i.Address) then, and then, only I am able to load the child entity.
Yes! this is the best way to load related data in EF Core.
You further said:
I already use the Include but is there any other option exist to load child entity if I get the parent entity.
Yes! There is! That is called Lazy loading. To enable lazy loading you have to make the navigation property virtual as follows:
public class Client
{
public int Id { get; set; }
public string LastName { get; set; }
public string Gender { get; set; }
public DateTime DateOfBirth { get; set; }
public virtual Address Address { get; set; } // <-- Here it is
}
And you have to register your DbConext as follows:
services.AddDbContext<BloggingContext>(
b => b.UseLazyLoadingProxies() // <-- Here is it is
.UseSqlServer(myConnectionString));
UseLazyLoadingProxies() method is available in the Microsoft.EntityFrameworkCore.Proxies nuget package.
Note: You cannot disable lazy loading for a certain query. So using Eager loading is the best way to load related data in EF Core.
In EF, there is a concept called Eager Loading using .Include.
MS Docs - Loading Related Data - EF Core
.NET Fiddle
using MyContext context = new MyContext();
IList<Client> clients =
context.Clients
.Include(c => c.Address)
.Where(c => c.LastName == "patel")
.ToList();
You can use Include()
Linq query
using (var context = new DBEntities())
{
var result = (from c in context.Client.Include("Address")
where c.IsActive
select c).ToList();
}
Lambda Expression
using (var context = new DBEntities())
{
var result = context.Client.Include(p => p.Address).Where(c => c.IsActive).ToList();
}

Creating copy of entities with many to many relationship without duplicating one of the type

I have problem with copying entities with many to many relationship.
I have three entities Company, Role and User defined like this:
Company:
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public virtual IList<User> Users { get; set; }
}
User:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public virtual IList<Role> Roles { get; set; }
}
Role:
public class Role
{
public int Id { get; set; }
public string Name { get; set; }
public virtual IList<User> Users { get; set; }
}
Also, I defined many to many relationship between users and roles:
public class UserConfiguration : EntityTypeConfiguration<User>
{
public UserConfiguration()
{
ToTable("TUser");
HasKey(x => x.Id);
HasMany(x => x.Roles).WithMany(x => x.Users).Map(m =>
{
m.MapLeftKey("UserId");
m.MapRightKey("RoleId");
m.ToTable("TUserRole");
});
}
}
I used migrations to create tables in db and obviously EF created table TUserRole (so far everything good).
And now, I would like to create copy of company and users but without copying roles (so I want to create new records at the tables TCompany, TUser and TUserRole, but no new records at the TRole).
I thought that something like this would work but I'm getting exception:
Context context = new Context();
var company = context.Companies.Include(x => x.Users.Select(u => u.Roles)).AsNoTracking().SingleOrDefault();
context.Companies.Add(company);
foreach (var user in company.Users)
{
foreach (var role in user.Roles)
{
context.Entry(role).State = EntityState.Unchanged;
}
}
context.SaveChanges();
And the exception is Saving or accepting changes failed because more than one entity of type 'Mackas.EF.Model.Role' have the same primary key value.
I understand why I'm getting this (because there is more than one role with the same ID), but I don't know what should be my approach.
Any suggestions?
I'm using EF 6.1.3.
Using AsNoTracking generally is a good idea to obtain a graph of entities that aren't attached to a context. As you know, adding the root entity (company) to a new context will mark all entities in the graph as Added and copying entities is a piece of cake.
But there's one bummer. AsNoTracking causes EF to materialize a new object for each entity in the result set, because it has no way to track that an entity has already been materialized. So you're OK as long as the object graph only diverges off the root entity. I.e. as long as all associations are 1 - 0..n. It that is true, all entities in the graph will represent exactly one "real" entity.
However, in your case, there's a m - n association between User and Roles. The graph converges. If some users have the same roles, EF will create duplicate Role objects when using AsNoTracking.
[By the way, contrary to EF6, EF-core manages to create unique entities even with AsNoTracking]
The way to go here is to query the object graph by one context, as POCOs, not proxies, and then add/attach it to a second context:
Company company;
using (Context context = new Context())
{
context.Configuration.ProxyCreationEnabled = false;
company = context.Companies.Include(x => x.Users.Select(u => u.Roles))
.SingleOrDefault();
}
using (Context context = new Context())
{
context.Companies.Add(company);
foreach (var user in company.Users.SelectMany(u => u.Roles)
.Distinct())
{
context.Entry(role).State = EntityState.Unchanged;
}
context.SaveChanges();
}
Proxies have a reference to the context they were created by, so you can't attach them to a second context.

Entity Framework and LINQ left join and include on same table

I have the following class structure for my Users and the permissions they're in for the different companies they may be associated to:
public class User
{
public Guid Id { get; set; }
public List<Permission> Permissions { get; set; }
public Company DefaultCompany { get; set; }
}
public class Permission
{
public User User { get; set; }
public Company Company { get; set; }
public int PermissionLevel { get; set; }
}
public class Company
{
public Guid Id { get; set; }
public string Name { get; set; }
}
This results in three SQL tables. There is a FK between Permission.User_Id > User.Id and Permission.Company_Id > Company.Id. There is no explicit relationship (ie. FK) between User.DefaultCompany and the Company table. This is on purpose due to a legacy choice in our database schema.
I also have a database repository method that grabs a user by it's Id, and includes the full Company record:
public User GetById(Guid Id)
{
return (from r in this.Context.Users.Include("Permissions.Company")
where r.Id == Id
select r)
.SingleOrDefault();
}
This works fine, but it doesn't set the DefaultCompany property. So I tried setting that by changing this method to the following. It's worth pointing out that the Company record that represents the DefaultCompany shares the same ID value as the User.
public User GetById(Guid Id)
{
return (from r in this.Context.Users.Include("Permissions.Company")
where r.Id == Id
join c in this.Context.Companies on r.Id equals c.Id into companies
from company in companies.DefaultIfEmpty()
select new { User = r, Company = company })
.ToList()
.Select(p => { p.User.DefaultCompany = p.Company; return p.User; })
.SingleOrDefault();
}
And this does, in fact, set the DefaultCompany but it has the side effect of not selecting the Permissions list. I can do this all as two separate operations, as in the following code, but I'd rather not hit the database twice if I don't have to.
public User GetById(Guid Id)
{
var u = (from r in this.Context.Users.Include("Permissions.Company")
where r.Id == Id
select r)
.SingleOrDefault();
u.DefaultCompany = (from r in this.Context.Companies where r.Id == u.Id select r).SingleOrDefault();
return u;
}
Is there another way to accomplish this?
Edit: explaining resulting SQL data model and additional example.
There are two possible solutions for this problem.
The cleanest is to use the Fluent API to indicate the model that there is a 1 to 1 relation between User and Company.
Override the OnModelCreating(DbModelBuilder modelBuilder) method of your context.
Inside it, configure the 1 ro 1 relation like this:
modelBuilder.Entity<User>()
.HasOptional(u => u.DefaultCompany)
.WithRequired();
NOTE: with this configuration there is a relationship of 1 user to 0 or 1 default companies (note the Optional in HasOptional). And the default company must have a User on the other side. When a 1 to 1 (or 1 to 0..1) relation is configured, EF will automatically use the PK of the related tables to create the relation between them. You can fine tune the relation using other Fluent API functions
After doing so, you can include the DefaultCompany using Include():
User user = ctx.Users
.Include(u => u.DefaultCompany)
.SingleOrDefault(u => u.Id == userId);
The other, more ugly solution, is to use your second query and include the missing permissions in the projection, to force EF to recover them from the DB.
// ...
select new { User = r, Company = company, Companies = r.Permissions }
// ...

Categories