Extending the ASP.NET Core Identity UserManager to set the Employee Id during registration
In previous posts I looked at adding a custom property to the ASP.NET Core Identity user, along with reading and writing the property for a logged in user. The next logical step, assuming that the notional HR application is "self-service" for registrations would be to set the EmployeeId property during identity creation. This requires a custom implementation of the UserManager class. This also allows the link between identity (logins) and those who should be allowed to register to be tightened up, meaning that new-starters would be able to register as part of their day one process - assuming the mythical HR department in this story are being diligent about putting employees information into their system before they arrive!
Related posts:
- Taking the GUID out of ASP.NET Core Identity
- Splitting out ASP.NET Core Identity into a separate library
- Extending the ASP.NET Core Identity user
- Reading and writing custom ASP.NET Core Identity user properties
- Extending the ASP.NET Core Identity UserManager to set the Employee Id during registration (this post)
- The finishing touches to hooking into ASP.NET Core Identity user creation
Extending the UserManager
The first thing to do is spin-up a new class that inherits from the built-in UserManager, which I'm going to type against my ApplicationIdentityUser so that it's fully aware of the additional properties I've added, rather than having to cast to/from inside the code:
public class ApplicationIdentityUserManager : UserManager<ApplicationIdentityUser> { public ApplicationIdentityUserManager(IUserStore<ApplicationIdentityUser> store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<ApplicationIdentityUser> passwordHasher, IEnumerable<IUserValidator<ApplicationIdentityUser>> userValidators, IEnumerable<IPasswordValidator<ApplicationIdentityUser>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<ApplicationIdentityUser>> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { } }
The constructor was automatically generated for me by Visual Studio using the Generate Constructor Quick Action (tip: You can make the list of available quick actions appear by pressing CTRL-ENTER) which removes this ugly attack of the red squiggle:
That was likely necessary to allow the resolution of all the various different generic type parameters that a concrete inplementation of UserManager requires.
Before I start adding code of any consequence to my custom UserManager, I want to make sure I can wire it into the application without things blowing up on me. In order to tell ASP.NET Core to use it, a small tweak to the ConfigureServices method in Startup.cs needs to be made, by adding a call to the extension method AddUserManager:
services.AddDefaultIdentity<ApplicationIdentityUser>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddUserManager<ApplicationIdentityUserManager>();
With that in place I'm going to start simple by putting a breakpoint on the constructor and running the application up. Hitting F5 (and then doing it again because I forgot to set the startup project as the Web App) spun everything up with the breakpoint in the constructor being hit a few seconds later, so that's a successful test.
So now I've got a customer UserManager created and hooked-up, the question becomes what to extend to achieve what I'm after? As my objective is to tweak the registration process, a look through the list of overridable methods suggests that there are a couple of likely candidates:
Task<IdentityResult> CreateAsync(ApplicationIdentityUser user) Task<IdentityResult> CreateAsync(ApplicationIdentityUser user, string password)
I'll start off by overriding both, setting a breakpoint in each and seeing which one (or both!) gets hit during the process of registering a new user.
As you can see from the stack trace, both actually get called. The method that just takes an instance of ApplicationIdentityUser gets called last, of the two, by which point the instance that's being passed around contains a hashed version of the users password. Because of that it seems eminently sensible to me to override the CreateAsync method that doesn't accept the users password as a parameter. If I don't have any code in the path that has access to it, I can't do anything stupid with it like write it to a log file on the web server ready for someone to download!
For simplicities sake I'm going to make a very simple change to the CreateUser method to populate the EmployeeId property and ensure it takes:
public override async Task<IdentityResult> CreateAsync(ApplicationIdentityUser user) { //TODO: Something sensible user.EmployeeId = DateTime.Now.Millisecond; return await base.CreateAsync(user); }
With that code in place, registering yet another new user certainly looks like it's worked as the home page after registration completes is showing the number 446 which I'm going to assume is the number of milliseconds of the Now when I registered the user.
As well as modifying the user before it's written to the store, the attempt to create the user can also be rejected by returning from IdentityResult.Failed instead of calling down and returning the result of calling base.CreateAsync. You do have to call the static Failed method on IdentityResult, neither of the instance properties have public setters. Say,
public override async Task<IdentityResult> CreateAsync(ApplicationIdentityUser user) { if (!user.UserName.Contains("allowed")) { return IdentityResult.Failed(new IdentityError { Description = "User names must contain the string 'allowed'" }); } user.EmployeeId = DateTime.Now.Millisecond; return await base.CreateAsync(user); }
If a user tries to register who doesn't have the string allowed in their email address, they'll receive an error message telling them this. This is the place where hooking it all together by integrating the Identity Database and Application Database will happen!