How to extend identity UserManager class
Tags: dotnet, aspnetcore, identity
Published: 2021-06-06 21:28:20

Recently I've answered a question on [StackOverflow](https://stackoverflow.com/q/67846618/1385614) 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.
```csharp
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`:
```csharp
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.
```csharp
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](https://github.com/dotnet/aspnetcore/blob/30e2d2265545c006c5feba3ebfdf39f7e2db3730/src/Identity/EntityFrameworkCore/src/UserStore.cs#L332) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lm18b2pzbicmyko4d9n1.jpg)
Inside `AddToRoleAsync` implementation `FindRoleAsync` method is called and I checked `FindRoleAsync` implementation:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qe9qlc3j0mk1zjl5dadc.jpg)
Inside `FindRoleAsync` `Role` property is used to fetch a role by name and again I check `Role` definition:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h4viiha3bq7acvd7okd7.jpg)
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()`:
```csharp
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`:
```csharp
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.