Fluent NHibernate only cascade delete of association record - nhibernate

We're using NHibernate for our membership system. A User can be a member of many Roles and a Role can have many users.
When a Role or a User is deleted, it should only cascade the delete of the association record ("RoleUsers" table).
Deleting a Role works as expected. However, deleting a User does not delete the association record and as such fails due to a foreign key constraint.
My mapping on the Role side:
HasManyToMany(r => r.Users)
.Access.CamelCaseField()
.Table("RoleUsers")
.ParentKeyColumn("RoleId")
.ChildKeyColumn("UserId")
.AsSet();
Mapping on the User side:
HasManyToMany(u => u.Roles)
.Access.CamelCaseField()
.Table("RoleUsers")
.ParentKeyColumn("UserId")
.ChildKeyColumn("RoleId")
.Inverse(); // we'll add user to role, not role to user
And the failing test:
[Test]
public void Deleting_user_should_not_delete_roles()
{
var user = new User("john#doe.com", "John", "Doe", "Secr3t");
var role = new Role("Admin");
role.AddUser(user);
object id;
using (var txn = Session.BeginTransaction())
{
id = Session.Save(user);
Session.Save(role);
txn.Commit();
}
Session.Clear();
var fromDb = Session.Get<User>(id);
using (var txn = Session.BeginTransaction())
{
Session.Delete(fromDb);
txn.Commit();
}
Session.Query<Role>().Count().ShouldEqual(1);
}
I've tried every combination of Cascade on the user mapping and it either fails or deletes the association record AND the role (not what I want).

Inverse and cascading are two different concepts. And of course, both are supported on <many-to-many> relation. See the documentation 6.8 (scroll down almost to 6.9)
http://nhibernate.info/doc/nh/en/index.html#collections-bidirectional
1) Firstly, we can remove the inverse setting of the User.Roles collection. This will straightens the behavior of the Users`s relations to Roles, and force their deletion before User itself is deleted.
2) Secondly. If the Roles collection of the User is marked as inverse,
HasManyToMany(u => u.Roles)
...
.Inverse(); // we'll add user to role, not role to user
deletion of any User, will never trigger deletion of the pair. That's because we are explicitly saying: the one and only one who care about the relation is the Role.
So if we would like to continue in your scenario:
.Inverse(); // we'll add user to role, not role to user
we should be consitent. "we'll remove user from roles, not roles from user":
[Test]
public void Deleting_user_should_not_delete_roles()
{
var user = new User("john#doe.com", "John", "Doe", "Secr3t");
var role = new Role("Admin");
role.AddUser(user);
object roleId;
object id;
using (var txn = Session.BeginTransaction())
{
id = Session.Save(user);
roleId = Session.Save(role);
txn.Commit();
}
Session.Clear();
// take both from DB
var userFromDb = Session.Get<User>(id);
var roleFromDb = Session.Get<Role>(roleId);
using (var txn = Session.BeginTransaction())
{
// HERE just remove the user from collection
roleFromDb.Users.Remove(userFromDb);
// all relations will be deleted
Session.Save(roleFromDb);
txn.Commit();
}
...
// assertions
// "John's" relation to Role "admin" is deleted
}
NOTE:
3) Cascade was not used in there, but could help to reduce Session.Save(user)...
EDIT: Extending the point 3)
Deletion of the user as Ben Foster noticed in a comment.
3) We should even allow the Role to manage its Users collection completely. Let's introduce the casdace="all" (casdace="all-delete-orhpan" if User without any Role should be deleted at all). Now we can add/update users only via Role object.
the mapping of the Role's Users collection should look like:
HasManyToMany(r => r.Users)
.Access.CamelCaseField()
.Table("RoleUsers")
.ParentKeyColumn("RoleId")
.ChildKeyColumn("UserId")
//.Cascade.All(); // just save or update instance of users
.Cascade.AllDeleteOrphan(); // will even delete User without any Role
.AsSet();
Having inverse and cascade we can adjust the test:
[Test]
public void Deleting_user_should_not_delete_roles()
{
var user = new User("john#doe.com", "John", "Doe", "Secr3t");
var role = new Role("Admin");
role.AddUser(user);
object roleId;
using (var txn = Session.BeginTransaction())
{
// I. no need to save user
roleId = Session.Save(role);
...
And later call this to get rid of a User
...
using (var txn = Session.BeginTransaction())
{
var user = Session.Get<User>(id);
var roles = user.Roles.ToList();
roles.ForEach(role => role.RemoveUser(user))
// II. not only relations, but even the User is deleted
// becuase updated roles triggered delete orhpan
// (no Session.Update() call there)
txn.Commit();
}

Related

How to get Role Claims?

I've created a user and attached to him a role that has a number of claims. The problem is I don't see a direct way to access retrieve them using Entity Framework Core and Identity integration. Here's what I'd like to do ideally:
return _context.Users
.Include(u => u.Roles)
.ThenInclude(r => r.Role)
.ThenInclude(r => r.Claims)
But there's not Role property, just RoleId. So I can not Include role claims. Of course I get make a separate query to get claims or even use RoleManager:
var user = _context.Users.Single(x => x.Id == ...);
var role = _roleManager.Roles.Single(x => x.Id == user.Roles.ElementAt(0).RoleId);
var claims = _roleManager.GetClaimsAsync(role).Result;
but it looks inefficient and even ugly. There should be a way to make a single query.
My last hope was Controller.User property (ClaimsIdentity). I hoped it somehow smartly aggregates claims from all the roles. But seems like it doesn't...
You can use SQL-like query expressions and get all claims from all roles of a user like this:
var claims = from ur in _context.UserRoles
where ur.UserId == "user_id"
join r in _context.Roles on ur.RoleId equals r.Id
join rc in _context.RoleClaims on r.Id equals rc.RoleId
select rc;
You can add navigation properties.
public class Role : IdentityRole
{
public virtual ICollection<RoleClaim> RoleClaims { get; set; }
}
public class RoleClaim : IdentityRoleClaim<string>
{
public virtual Role Role { get; set; }
}
Then you have to configure your identity db context:
public class MyIdentityDbContext : IdentityDbContext<User, Role, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, RoleClaim, IdentityUserToken<string>>
Usage:
await _context.Roles.Include(r => r.RoleClaims).ToListAsync();
At the end it generates the following query:
SELECT `r`.`Id`, `r`.`ConcurrencyStamp`, `r`.`Name`, `r`.`NormalizedName`, `r0`.`Id`, `r0`.`ClaimType`, `r0`.`ClaimValue`, `r0`.`RoleId`
FROM `roles` AS `r`
LEFT JOIN `role_claims` AS `r0` ON `r`.`Id` = `r0`.`RoleId`
ORDER BY `r`.`Id`, `r0`.`Id`
Source: Identity model customization in ASP.NET Core
Make sure you are adding the roles and claims correctly. Below is an example of how I create a user and add claims and roles.
private async Task<IdentityResult> CreateNewUser(ApplicationUser user, string password = null){
//_roleManger is of type RoleManager<IdentityRole>
// _userManger is of type UserManager<ApplicationUser>
//and both are injected in to the controller.
if (!await _roleManger.RoleExistsAsync("SomeRole")){
await _roleManger.CreateAsync(new IdentityRole("SomeRole"));
}
var result = password != null ? await _userManager.CreateAsync(user, password) : await _userManager.CreateAsync(user);
if(result.Succeeded) {
await _userManager.AddToRoleAsync(user, "SomeRole");
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Name, user.Email));
}
return result;
}
Then you can use the _userManager to get the claims. This is how I get the current user using _userManager. Then you can just call something like this:
var claims = await _userManager.GetClaimsAsync(user);

ASP.NET Core Identity Role, Claim and User

I am an ASP.NET Core beginner. I'm stuck in role, claim and user relationship.
I have a user Ben, user belongs to Admin role. Admin role has claims view-page and edit-page in database.
But I can't get claims and roles to be belonging to that user:
(Please see comment in code)
var user = await _userManager.FindByNameAsync(applicationUser.UserName);
if(user != null) {
var userClaims = await _userManager.GetClaimsAsync(user); // empty, WHY ?
var userRoles = await _userManager.GetRolesAsync(user); // ['admin']
var adminRole = DbContext.Roles.FirstOrDefault(x => x.Name == "Admin");
IList<Claim> adminClaims;
if(adminRole != null)
{
adminClaims = await _roleManager.GetClaimsAsync(adminRole);
// correct => ['view-page', 'edit-page']
}
}
}
In my mind, I understand when a user is a member of a role, he inherit that role's claims.
Default ASP.NET Identity have 5 tables:
Users.
Roles.
UserRoles - A user can have many roles.
RoleClaims - A role can have many claims.
UserClaims - A user can have many claims.
Do i think correct ? Why userManager.GetClaimsAsync(user) returns empty claims ?
Any suggestion?
Why userManager.GetClaimsAsync(user) returns empty claims ?
Because UserManager.GetClaimsAsync(user) queries the UserClaims table. Same for
RoleManager.GetClaimsAsync(role) queries the RoleClaims table.
But by design in ASP.NET Identity Core when a user is a member of a role, they automatically inherit the role's claims. You can check the ClaimsPrincipal, for example inside a controller action:
var claims = User.Claims.ToList();
You can see the code in UserClaimsPrincipalFactory.cs that creates a ClaimsPrincipal from an user.
I have had to deal with this issue recently and to solve the problem of locating Users by a particular Claim that came from a Role is to create a new Claim object with the values from the Role Claim:
var role = await roleManager.FindByNameAsync(yourRoleName);
if(role != null)
{
var roleClaims = await roleManager.GetClaimsAsync(role);
if(roleClaims != null && roleClaims.Count() > 0)
{
foreach(var claim in roleClaims.ToList())
{
var users = await userManager.GetUsersForClaimAsync(new Claim(claim.Type, claim.Value));
if(users != null && users.Count() > 0)
{
foreach(var user in users.ToList())
{
//This is an example of only removing a claim, but this is the
//area where you could remove/add the updated claim
await userManager.RemoveClaimAsync(user, new Claim(claim.Type, claim.Value));
}
}
}
}
}
This allowed me to Update/Delete a role with claims and pass those changes to the Users to be Re-Issued/Removed that were assigned the roles and claims. However, I am still looking for something more elegant/easier with less code.

NHibvernate 3.2 many to many not populating join table

I have two classes, user and role, defined as:
public class User : Entity
{
// other properties ...
public virtual string Username
public virtual ICollection<Role> Roles { get; set; }
}
public class Role : Entity
{
public virtual string Name { get; set; }
public virtual ICollection<User> Users { get; set; }
}
In my mapping code, I have the following:
mapper.Class<User>(map =>
{
map.Bag(x=>x.Roles,
cm=>
{
cm.Table("UserRole");
cm.Cascade(Cascade.All);
cm.Key(k => k.Column("[User]"));
},
em=>
{
em.ManyToMany(mm =>
{
mm.Column("[Role]");
});
});
});
mapper.Class<Role>(map =>
{
map.Bag(x=>x.Users,
cm=>
{
cm.Inverse(true);
cm.Table("UserRole");
cm.Key(k=>k.Column("[Role]"));
},
em =>
{
em.ManyToMany(mm =>
{
mm.Column("[User]");
});
});
});
The mappings generate the expected schema, but the join table is never populated. Adding a new user with a new Role in its collection persists the role and then the user to the appropriate tables, but the join table is left empty. Why?
Edit: I still have not made any progress on this. I'm absolutely sure the mapping is correct, and the correct schema is generated, but the join table simply isn't populated. For test purposes, I'm generating entities using NBuilder like so:
var roles = new Role[]
{
new Role("Admin"),
new Role("Manager"),
new Role("User")
};
var users = Builder<User>.CreateListOfSize(10)
.TheFirst(1)
.Do(x =>
{
x.Roles.Add(roles[0]);
x.Roles.Add(roles[1]);
roles[0].Users.Add(x);
roles[1].Users.Add(x);
})
.All()
.With(x => x.Id = 0)
.And(x => x.Version = 0)
.And(x => x.Username = "test user")
.And(x => x.Password = "test password")
.Do(x =>
{
x.Roles.Add(roles[2]);
roles[2].Users.Add(x);
}
.Build();
foreach (var u in users) session.Save(u);
The User and Role entities are persisted correctly, but the join table remains empty. This means I cannot effective query the roles for a given user later, which nullifies the point.
Make sure you have both classes referencing each other.
I think that code, similar to one below, should work for you:
role.Users.Add(user);
user.Roles.Add(role);
session.Save(user); // NH only saves user and role, so it can get auto-generated identity fields
session.Flush(); // NH now can save into cross-ref table, because it knows required information (Flush is also called inside of Transaction.Commit())
I found a good answer to a question about many-to-many with lot of explanations and quotes from NH documentation. I think it worth to read it.
[EDIT]
In answer to this somewhat similar question there is discussion in which need for explicit transaction to save into cross-table is mentioned.
I also edited code above with adding session.Flush() to reflect my findings.
I ended up downloading the NHibernate source and referencing that directly so I could step through it. It turns out that it had something to do with the fact that my code for generating the test data was not wrapped in an explicit session transaction. Once I added that, it was fine. I'd love to see some kind of explanation on this, as I wasn't able to follow the code very clearly, but I'm at least satisfied that the problem is solved.

NHibernate query collection

I have a users and roles table. A user can have multiple roles.
I want to grab all of the users without a specific role. The problem is if a user has 2 roles, one being the role we don't want, the user will still be returned.
public IList<User> GetUserByWithoutRole(string role)
{
return CreateQuery((ISession session) => session.CreateCriteria<User>()
.CreateAlias("Roles", "Roles")
.Add(!Restrictions.Eq("Roles.RoleDescription", role))
.List<User>());
}
The only solution I came up with was client side
public IEnumerable<User> GetUserByWithoutRole(string role)
{
return CreateQuery((ISession session) => session.CreateCriteria<User>()
.CreateAlias("Roles", "Roles")
.Add(!Restrictions.Eq("Roles.RoleDescription", role))
.List<User>()).Where(u => u.Roles.FirstOrDefault(r => r.RoleDescription == role) == null);
}
Anyone know of a better solution? Thanks!
Alternatively you can use Criteria API to create subquery
var subquery = DetachedCriteria.For<Role>("role");
subquery.Add(Restrictions.EqProperty("role.User.id", "user.id"))
.SetProjection(Projections.Property("role.RoleDescription"));
var users = session.CreateCriteria<User>("user")
.Add(Subqueries.NotIn(role, subquery))
.List<User>();
If you want to do that in nhibernate you'll have to do two queries or a subquery. Here is a similar post:
FluentNHibernate query on many-to-many relationship objects
Thanks, for anyone interested this is what I finished up with:
public IEnumerable<User> GetUserByWithoutRole(string role)
{
var subQuery = DetachedCriteria.For<User>()
.CreateAlias("Roles", "Roles")
.SetProjection(Projections.Property("UserID"))
.Add(Restrictions.Eq("Roles.RoleDescription", role));
return _session.CreateCriteria<User>()
.SetResultTransformer(new DistinctRootEntityResultTransformer())
.Add(Subqueries.PropertyNotIn("UserID", subQuery))
.AddOrder(Order.Asc("FirstName"))
.List<User>();
}

Fluent NHibernate one-to-many Cascade.SaveUpdate() blocks updates to entity

We have an Enrollment object that has a Student object and the Student object has many Enrollment objects. If I leave off the Cascade.SaveUpdate() from the Enrollment's Student reference, updates to the Student table do not execute, but updates to the Enrollment object succeed. But if I add the Cascade.SaveUpdate() on the Enrollment's Student reference, the updates to the Student table work fine, but updates to the Enrollment table fail. No exceptions are thrown, the updates just don't succeed.
There must be some way to be able to save objects on both sides of the relationship, but what am I missing?
Here's the code snips, let me know if you need more:
EnrollmentMap:
References(x => x.Student)
.Column("student_id");// without the cascade on the next line, this fails to update changes to Student
//.Cascade.SaveUpdate();// when uncommented this updates changes to Student but blocks updates to Enrollment
StudentMap:
HasMany(x => x.Enrollments)
.KeyColumn("student_id")
.Inverse()
.Cascade.SaveUpdate();
Database call:
public Application GetApplication(long applicationId)
{
using (var session = sessionFactory.OpenSession())
{
var query = session.Linq();
query.Expand(x => x.Enrollment);
query.Expand(x => x.Enrollment.Student);
var result = from entity in query
where entity.ApplicationId == applicationId
select entity;
return result.Count() > 0 ? result.First() : null;
}
}
Database save:
using (var session = sessionFactory.OpenSession())
{
using (var transaction = session.BeginTransaction())
{
try
{
session.SaveOrUpdate(entity);
transaction.Commit();
}
catch(Exception ex)
{
transaction.Rollback();
throw;
}
}
}
You should try and load your entity in the same session as you update it. I think that is your problem.
If you really can't do this, then it is possible to 'merge' the entity in to your session (google 'NHibernate merge').