How to extend identity UserManager class

moesmp

Mohsen Esmailpour

Posted on June 6, 2021

How to extend identity UserManager class

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);
    }

    ....
}


Enter fullscreen mode Exit fullscreen mode

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)
    {
    }
}


Enter fullscreen mode Exit fullscreen mode

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;
    }
}


Enter fullscreen mode Exit fullscreen mode

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)
Alt Text
Inside AddToRoleAsync implementation FindRoleAsync method is called and I checked FindRoleAsync implementation:
Alt Text
Inside FindRoleAsync Role property is used to fetch a role by name and again I check Role definition:
Alt Text
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;
}


Enter fullscreen mode Exit fullscreen mode

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();


Enter fullscreen mode Exit fullscreen mode

The last thing I want to mention is that if you have extended IdentityUser, IdentityRole, ... classes, replace them with your own implementation.

💖 💪 🙅 🚩
moesmp
Mohsen Esmailpour

Posted on June 6, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related