How to extend identity UserManager class
Mohsen Esmailpour
Posted on June 6, 2021
Recently I've answered a question on StackOverflow website and the question was how to create an overloaded method for both AddToRoleAsync()
and IsInRoleAync()
to look for the role by its ID and not by name. I this post I show how to extend UserManager
class in the way it is implemented and access some internals of UserManager
.
First way to extend the UserManager
is using extension methods. In some cases you don't need to create another class to extend UserManager
and by implementing some extensions methods to achieve what you want.
public static class UserManagerExtensions
{
public static Task<ApplicationUser> FindByNameAsync(this UserManager<ApplicationUser> userManager, string name)
{
return userManager?.Users?.FirstOrDefaultAsync(um => um.UserName == name);
}
public static Task<ApplicationUser> FindByCardIDAsync(this UserManager<ApplicationUser> userManager, string cardId)
{
return userManager?.Users?.FirstOrDefaultAsync(um => um.CardId == cardId);
}
....
}
In some cases like create an overloaded method for AddToRoleAsync()
to add a role to a user by role ID and not by role name, the extension method doesn't work and it requires a custom implementation of UserManager
class. Lets spin up a new class that inherits from the built-in UserManager
:
public class ApplicationUserManager : UserManager<IdentityUser>
{
public ApplicationUserManager(
IUserStore<IdentityUser> store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<IdentityUser> passwordHasher,
IEnumerable<IUserValidator<IdentityUser>> userValidators,
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<IdentityUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
}
}
You can generate the constructor automatically with Visual Studio by using the Generate Constructor Quick Action (by pressing CTRL-ENTER).
The first parameter of constructor is store
and for implementing another method to add role to a user by identifier we need that.
public class ApplicationUserManager : UserManager<IdentityUser>
{
private readonly UserStore<IdentityUser, IdentityRole, ApplicationDbContext, string, IdentityUserClaim<string>,
IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
_store;
public ApplicationUserManager(
IUserStore<IdentityUser> store,
...)
{
_store = (UserStore<IdentityUser, IdentityRole, ApplicationDbContext, string, IdentityUserClaim<string>,
IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>)store;
}
}
Before explaining why I cast the interface of _store
to generic UserStore
class with 9 parameters, let's look at the implementation AddToRoleAsync method (in such a case as good practice always check the source code of identity to get an idea to implement what you want)
Inside AddToRoleAsync
implementation FindRoleAsync
method is called and I checked FindRoleAsync
implementation:
Inside FindRoleAsync
Role
property is used to fetch a role by name and again I check Role
definition:
I noticed I cannot use the Role
property to the get role by identifier because it's private property but I noticed there is a public property of DbContext
that I can use. So that's why I cast IUserStor
interface to UserStore
class to have access to Context
property.
It's time to implement AddToRoleAsync()
and IsInRoleAync()
:
public virtual async Task<IdentityResult> AddToRoleByRoleIdAsync(IdentityUser user, string roleId)
{
ThrowIfDisposed();
if (user == null)
throw new ArgumentNullException(nameof(user));
if (string.IsNullOrWhiteSpace(roleId))
throw new ArgumentNullException(nameof(roleId));
if (await IsInRoleByIdAsync(user, roleId, CancellationToken))
return IdentityResult.Failed(ErrorDescriber.UserAlreadyInRole(roleId));
_store.Context.Set<IdentityUserRole<string>>().Add(new IdentityUserRole<string> { RoleId = roleId, UserId = user.Id });
return await UpdateUserAsync(user);
}
public async Task<bool> IsInRoleByIdAsync(IdentityUser user, string roleId, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
throw new ArgumentNullException(nameof(user));
if (string.IsNullOrWhiteSpace(roleId))
throw new ArgumentNullException(nameof(roleId));
var role = await _store.Context.Set<IdentityRole>().FindAsync(roleId);
if (role == null)
return false;
var userRole = await _store.Context.Set<IdentityUserRole<string>>().FindAsync(new object[] { user.Id, roleId }, cancellationToken);
return userRole != null;
}
I used _store.Context.Set<IdentityRole>()
to find by a role by identifier.
Next step is to register ApplicationUserManager
:
services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = false)
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddUserManager<ApplicationUserManager>() // Add ApplicationUserManager
.AddDefaultTokenProviders()
.AddDefaultUI();
The last thing I want to mention is that if you have extended IdentityUser
, IdentityRole
, ... classes, replace them with your own implementation.
Posted on June 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.