The property 'RoleId' on entity type 'UserRole' is part of a key and so cannot be modified or marked as modified - .net-core

In my .NET Core application with EF Core I get the following error:
The property 'RoleId' on entity type 'UserRole' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.'
public class UserRole
{
public Guid RoleId { get; set; }
public virtual Role Role { get; set; }
public Guid UserId { get; set; }
public virtual User User { get; set; }
}
Why does it suggest to delete the row first and then create a new one. It should only update the role of the object and save it with the new one.
I have already tried to set the navigation properties (Role and User) to null, so it needs only to update the RoleId field with the new existing Role.Id.
I have tried to update the Role navigation itself with the new one.
But it doesn't matter.
What should I do to solve this?

A table like UserRole is a Many to Many joining table. In EF6 declaring these was optional where as long as the table just contained the UserID and RoleID, your User entity could simply have a Roles collection and the Role could optionally have a Users collection. When set up this way IMO it was easier to understand the limitation in that you wouldn't just take an existing Role reference and try to change it's ID.
AFAIK with EF Core you still need to map the joining table so User will have a collection of UserRoles and you have to navigate through that collection to get to the Roles from the User. Still, the behaviour for many-to-many relationships is the same, you do not modify these references, you need to remove and recreate them.
For instance, given:
var user = context.Users.Include(x => x.UserRoles).ThenInclude(x => x.Roles).Single(x => x.UserId == userId);
var newRole = context.Roles.Single(x => x.RoleId == newRoleId);
var userRole = user.UserRoles.SingleOrDefault(x => x.RoleId == oldRoleId);
//(Incorrect)
if (userRole != null)
userRole.Role = newRole;
//(Correct)
if (userRole != null)
user.UserRoles.Remove(userRole);
user.UserRoles.Add(new UserRole { User = user, Role = newRole });

Related

How do you add a new user and claim in a single transaction using ASP.NET Core 2.1 Identity?

I am trying to add a new user and some other associated entities including a claim as one transaction. My classes are basically defined as below. Note that I am using int primary keys on my User class and that it is an identity column (value is provided by the database on insertion).
public class User : IdentityUser<int>
{
// custom props here
}
public class UserClaim : IdentityUserClaim<int>
{
// i actually haven't added anything to this class, it's mainly here so
// i don't have to keep specifying the user's primary key type
}
public class OtherEntity
{
public int UserId { get; set; }
[ForeignKey(nameof(UserId))]
public User User { get; set; }
// other stuff
}
I then want to add the user etc. to the database something like this:
User user = new User(){ /* set props */ };
OtherEntity other = new OtherEntity()
{
User = user
};
UserClaim claim = new UserClaim()
{
/* this is where the problem is */
UserId = user.Id,
ClaimType = "my-claim-type",
ClaimValue = "my-claim-value"
};
context.AddRange(user, other, claim);
context.SaveChanges();
I can easily link the User to the OtherEntity because I have set up the navigation property so I can just add the User to it and entity framework takes care of the filling in the UserId column. I cannot do this with UserClaim because it doesn't have the navigation property. I could call context.SaveChanges() after adding the User and entity framework would get the User.Id created by the database for me which I could use to set UserId on the UserClaim, but that would mean two transactions.
I have tried adding the navigation property to my definition of UserClaim as follows:
public class UserClaim : IdentityUserClaim<int>
{
[ForeignKey(nameof(UserId))]
public User User { get; set; }
}
But I get following runtime error:
InvalidOperationException: The relationship from 'UserClaim.User' to 'User' with foreign key properties {'UserId' : int} cannot target the primary key {'Id' : int} because it is not compatible. Configure a principal key or a set of compatible foreign key properties for this relationship.
Is there a way of creating both the user, and the claim in the same transaction?
My question should have been: "How do I add navigation properties between ASP.NET Identity classes?"
If I had looked for the answer to that I would have found the microsoft docs explaining how to do it!
The docs linked above tell you how to add the User.UserClaims navigation property:
public class User : IdentityUser<int>
{
public virtual ICollection<UserClaim> Claims { get; set; }
}
public class DataContext : IdentityDbContext</* Identity classes */>
{
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<User>(e =>
{
e.HasMany(u => u.Claims)
.WithOne()
.HasForeignKey(c => c.UserId)
.IsRequired();
});
}
}
But it doesn't show how to make the reverse UserClaim.User navigation property. I worked out that this can be done like this:
public class UserClaim : IdentityUserClaim<int>
{
public virtual User User { get; set; }
}
public class DataContext : IdentityDbContext</* Identity classes */>
{
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<User>(e =>
{
e.HasMany(u => u.Claims)
.WithOne(c => c.User) // <- this line is different
.HasForeignKey(c => c.UserId)
.IsRequired();
});
}
}
You can then create a new user and claim at the same time as per my question:
User user = new User(){ /* set props */ };
UserClaim claim = new UserClaim()
{
User = user, // <- this is the bit you can't do without the nav. property
ClaimType = "my-claim-type",
ClaimValue = "my-claim-value"
};
context.AddRange(user, claim);
context.SaveChanges();
I guess it's common sense, though I didn't realise until I inspected the actual SQL hitting the database, but this will still require two trips to the database even though we are only calling .SaveChanges() once! Entity Framework will first save the User and get the ID for the inserted row which it will then use when inserting the UserClaim.
Inserting related data is documented in Entity Framework: https://learn.microsoft.com/pl-pl/ef/core/saving/related-data
And it is also well described in other topics, for example: Entity Framework Foreign Key Inserts with Auto-Id
Every of you entites need to be set correctly for relations (foreign keys) in your entites models (without them, EF don't know what to do) and when you are adding it, you need to start from beginning, so UserClaim must be set from your User entity, for example
var user = new User(){
//properites
OtherEntity = new OtherEntity()
{
Id = 0, /*will be set automatically*/
UserId = 0 /*will be set automatically*/
/* other properites */
};
Claim = new UserClaim(){
Id = 0, /*will be set automatically*/
UserId = 0 /*will be set automatically*/
/* other properites */
}
}
ontext.Add(user);
context.SaveChanges();
You didn't provide all the information about your relations, I've just assumed this from your code.
PS. AddRange has only one parameter.
EDIT:
To clarify my answer, for everything to work, AddRange/Add need to be called with your base entity and relations inside, in tree manner.
But first you need to configure your models, something like that.
public class User : IdentityUser<int>
{
[ForeignKey("UserClaimId")]
public virtual UserClaim Claim {get;set;}
}
public class UserClaim : IdentityUserClaim<int>
{
[ForeignKey("UserId")]
public virtual User User {get;set;}
}
public class OtherEntity
{
public int UserId { get; set; }
[ForeignKey("UserId")]
public User User { get; set; }
// other stuff
}
You can also use OnModelCreating to set up entites, as described in documentation: https://learn.microsoft.com/en-us/ef/core/modeling/relationships
Entity Framework right now don't know anything about relations in Database, data types etc.
Updated code example
Edit, don't have enough reputation to add comment to your answer
Well, EF still needs to track entities. I would need to write and run some examples to check if there is some way to improve trips to database. Maybe someone could tell you more about mechanism that is behind adding related-data and if there is a way to improve it.
There are some libraries that helps with improving performance of saving changes to database, for example: https://entityframework-extensions.net
You could try it.

asp.net ef code first duplicate elements with foreign key

I have the following code :
public Exam CreateExam(string name, List<Question> questions, DateTime timeNow)
{
User user = GetUserByName(name);
Exam exam = new Exam()
{
Questions = questions,
StartDate = timeNow,
User = user
};
Context.Exams.Add(exam);
Context.SaveChanges();
return exam;
}
Exam :
public class Exam
{
public int Id { get; set; }
public DateTime StartDate { get; set; }
public User User { get; set; }
public virtual List<Question> Questions { get; set; }
}
And user has the basic user infos.
My problem is that when I create an exam for the user, the save change also add a new user to the database, with only the ID being different. How do I prevent that and make it understand that I want to link it to the existing user ?
Thank you !
Edit: GetUserByName() :
Context.Database.SqlQuery<User>("Select * from Users where name = #name", new SqlParameter("name", name)).FirstOrDefault();
When you use SqlQuery to fetch user, he will not be tracked by Entity Framework, because you can write arbitrary query and map result to User class. So EF consider, that he is new one, when you reference to him. To fix this problem, you should manually attach his to context:
User user = GetUserByName(name);
Context.Users.Attach(user);
Based Slava Utesinov answer Entity Framework's change tracker does not track entity changes when get from raw query such as:
Context.Database.SqlQuery<User>("Select * from Users where name = #name", new SqlParameter("name", name)).FirstOrDefault();
Therefore when you call SaveChanges() method, Entity Framework change tracker detect user entity state as Added (new entity)
1.you can use DbSet.SqlQuery instead of Database.SqlQuery because DbSet.SqlQuery will tracked by the context:
Context.Users.SqlQuery("Select * from Users where name = #name", new SqlParameter("name", name)).FirstOrDefault();
or
2.you can attach entity to current context dbset as unchanged state

ASP.NET MVC 4 edit/delete operation

i have some question regarding to edit/delete operation via asp.net mvc or web applications in general.
Lets say i have the following url to delete or edit records : app/delete/5, app/edit/5 to edit or delete record with id 5.
When calling the link, a confirmation page is loaded via HTTP GET and the edit/delete operation itself is done via HTTP POST.
Once user knows the link, how can i prevent him from calling any other app/delete or app/edit for records he does not own or is not permitted to edit/delete? Example: app/delete/7312
What do i have to put in my controller action that the user can only fetch the confirmation page via GET for the records he is permitted to execute an edit/delete?
Best regards
marc
Okay at first I posted a simpler answer but I didn't understand the question. You are going to need two things. A way to know who created what in the system, and then some business logic in the delete action checking if the current user is equal to the user who created it. I would suggest this inheritable class that i use to track ownership of an object:
public class BaseEntity
{
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = false)]
[Display(Name = "Date Created")]
[DataType(DataType.Date)]
public DateTime? DateCreated { get; set; }
public string UserCreated { get; set; }
[Display(Name = "Date Modified")]
[DataType(DataType.Date)]
public DateTime? DateModified { get; set; }
public string UserModified { get; set; }
}
Then in your database context class, you can overwrite save changes to automatically track these things whenever a user in the system saves something t o populate those fields. This is handy because you don't have to go into Create, Edit, etc. and manually link this up.
public override int SaveChanges()
{
var entities = ChangeTracker.Entries().Where(x => x.Entity is BaseEntity && (x.State == EntityState.Added || x.State == EntityState.Modified));
var currentUsername = HttpContext.Current != null && HttpContext.Current.User != null
? Users.Find(HttpContext.Current.User.Identity.GetUserId()).Name
: "Anonymous";
foreach (var entity in entities)
{
if (entity.State == EntityState.Added)
{
((BaseEntity)entity.Entity).DateCreated = DateTime.Now;
((BaseEntity)entity.Entity).UserCreated = currentUsername;
}
((BaseEntity)entity.Entity).DateModified = DateTime.Now;
((BaseEntity)entity.Entity).UserModified = currentUsername;
}
return base.SaveChanges();
}
Then finally, to actually achieve your functionality, you can in your controller's delete action only let the delete go through if Users.Find(HttpContext.Current.User.Identity.GetUserID()).name is equal to the name of whoever created the model.
I know this is a lot, if you have questions please comment I'll check in.

Edit model with virtual field

I have a Shop model which contains several fields. One of which is a virtual User one. Whenever I try to edit one entry I get an error saying that User field is required.
public class Shop
{
//..
public virtual ApplicationUser User { get; set; }
//..
}
My workaround is this:
shop.User = shop.User; //re-set the value
shop.Active = true;
db.Entry(restaurant).State = EntityState.Modified;
db.SaveChanges();
And I have to do this for all the fields. Is this the standard approach for this or is there a better way?
Change your model to this:
public class Shop
{
//..
public int UserId {get; set; }
public virtual ApplicationUser User { get; set; }
//..
}
Entity Framework will automatically detect that UserId is the foreign key for object User. You had this problem because User is virtual (lazy loaded). When changing the model without accessing or setting this property EF thinks it's empty (I assume). The foreign key UserId is not virtual, and will be fetched together with the other properties of model Shop, so you don't have to re-set the value when saving the model.
To set a new user, you now have to do for example:
myShop.UserId = 1; // instead of setting myShop.User
For more information, see this article: http://msdn.microsoft.com/en-us/data/jj713564.aspx

Why entity framework does not delete record when setting navigation property to null

I am new with EF Code First and have some troubles with it.
Here my Model
public class User
{
public string Id { get; set; }
public string Name { get; set; }
public virtual Address Address { get; set; }
}
public class Address
{
public string Id { get; set; }
public string Street { get; set; }
public string Nr { get; set; }
}
What I want is, if I write the following code:
user.Address = null;
the related Address (record in database) should be deleted, but it is not! It removes only the foreign key in Users table. Also when I assign a new Address:
user.Address = new Address() { ... };
it is created a second record in database, why?
I want:
if assign null to user.Address the database record should be deleted
if assign a new Address object to user.Address it should replace the existing records data with the new one
How can I solve this?
If you need to delete an entity you need to mark it as deleted. Setting a navigation property to null will not delete the related entity. Note that there may be many navigation properties (different entities) pointing to the related entity and they all would be affected in the scenario you are describing.
Because you create a new Address a new entity is created. Then you set your navigation property to a newly created property this breaks the old relationship and creates a new one. Since the entity you created is not in the database it will be added. You also did not delete the old entity so it was not deleted from the database. If you just want to change property values of an entity just set properties to new values and invoke SaveChanges. This should update data in the database.

Resources