Notes on Adding .NET Core 2.2 Identity to Existing Project

Even though all my projects are in .NET Core now, I rarely get the opportunity to use Identity because of my work with our backend Legacy system.  Recently, though, I built a very lightweight SEO Management system for one of our sites (that allows a 3rd party to tweak our page titles, meta tags, etc) and wanted to give them user access and roles.

The entire project can be found on GitHub, but below is just a running list of sites I used to get this done, noting all the troubleshooting and stupid little mistakes I did along the way.

Adding Identity to an Already Existing Project

This part was relatively easy and the Microsoft documents provided an easy enough guide.  I believe I had some issues:

CS1902 C# Invalid option for /debug; must be full or pdbonly – with the Data Migrations because I didn’t have EntityFramework installed.   Basically, all errors in this phase were not as presented – they were mostly because I was lacking packages to migrate.

To ensure I had all the right packages installed, i used: Microsoft.AspNetCore.App -Version 2.2.6

Also, in this area, I decided not to put the connection string in appsettings.json, opting instead to use System Environment Variables both in development and in the future on Azure.  I changed my IdentityHostingStartup.cs file to this:

public void Configure(IWebHostBuilder builder)
{
    builder.ConfigureServices((context, services) =>
    {
        services.AddDbContext<DbContext>(options =>
        options.UseSqlServer(System.Environment.GetEnvironmentVariable("SQLAZURECONNSTR_Production")));

Configuring Email

I wanted to test everything first, with no email confirmation.  So, using Microsoft Docs I added this email class.  The only change is again, I prefer System Environment Variables, so my EmailSender class differs:

public class EmailSender : IEmailSender
{
    private string apiKey = System.Environment.GetEnvironmentVariable("SENDGRID_APIKEY");

    public Task SendEmailAsync(string email, string subject, string message)
    {
        return Execute(subject, message, email);
    }

    public Task Execute(string subject, string message, string email)
    {
        var client = new SendGridClient(apiKey);
        var msg = new SendGridMessage()
        {
            From = new EmailAddress("webmaster@mycompany.com", "SEO Management"),
            Subject = subject,
            PlainTextContent = message,
            HtmlContent = message
        };
        msg.AddTo(new EmailAddress(email));

        // Disable click tracking.
        // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
        msg.SetClickTracking(false, false);

        return client.SendEmailAsync(msg);
    }
}

Once I finished with the Microsoft Docs email setup, I started projected, registered 1 user (under email I want to be super admin).

Customizing User Roles & Seeding Admin

Now, I needed to assign that user the role of super admin and also create other user roles.  I used the following answer to get started added both methods to Startup.cs in the Configure method.

https://stackoverflow.com/questions/55994082/how-to-create-roles-in-asp-net-core-2-2-and-assign-them-to-users?answertab=votes#tab-top

            app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
 
 
    // this is just to seed other admin role, run once
    //CreateAdminRole(serviceProvider).Wait();

}
 
// this is just to seed other admin role, run once
private async Task CreateAdminRole(IServiceProvider serviceProvider)
{
    var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
    var UserManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
 
    IdentityResult roleResult;
 
    var roleCheck = await RoleManager.RoleExistsAsync("Admin");
    if (!roleCheck)
    {
        roleResult = await RoleManager.CreateAsync(new IdentityRole("Admin"));
    }
 
    ApplicationUser user = await UserManager.FindByEmailAsync("myemail@mycompany.com");
    await UserManager.AddToRoleAsync(user, "Admin");
}
 

After following that guide, the one error I kept getting here was:  No service for type ‘Microsoft.AspNetCore.Identity.RoleManager’.   

and another:  Unable to resolve service for type ‘Microsoft.AspNetCore.Identity.IRoleStore`1[Microsoft.AspNetCore.Identity.IdentityRole]’ while attempting to activate ‘Microsoft.AspNetCore.Identity.RoleManager`1[Microsoft.AspNetCore.Identity.IdentityRole]’.

The first was because I simply forgot to .AddRoles to the configuration of the DefaultIdentity.  The second error is that ORDER MATTERS.  AddRoles goes before AddEntityFramework.

Both resolved with this:

services.AddDefaultIdentity<ApplicationUser>()
            .AddRoles<IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>();

Finishing with the guide, I discovered another page that needed changing: _ManageNav which also had a SignIn @inject (like _LoginPartial) that needed to be changed:

@inject SignInManager<ApplicationUser> SignInManager

And then: InvalidOperationException: Unable to resolve service for type ‘Microsoft.AspNetCore.Identity.UserManager`1[

Which, again, is all pointing to the use of IdentityUser instead of your new ApplicationUser.  You’ll need to go through each of your Razor pages and change IdentityUser to ApplicationUser.

Which will lead to you scaffolding the views (if you hadn’t already) …

Scaffolding UI for Access to Razor Pages & Views

Now that the set up was working, I wanted a better look at my pages and views.  By default, in Core 2.1 the UI comes in a prebuilt package, but you can easily scaffold it to view and change as you like: ASP.NET Core 2.2 – Scaffold Identity UI.  I even did this over (ie, a 2nd time) over my first steps and it recognized all the previous code and just scaffolded nicely, no harm done.

Adding User Roles

At this point, any user can register, but they can not log in unless they confirm their email.  Once they DO confirm their email, they can log in but have no access to any of the pages because they have yet to be assigned a role.  The administrator must do this before they can continue.

Using the same method that I used to create the seed admin, I added a method to startup to seed the user roles:

// this is just to seed user roles, run once
private async Task CreateUserRoles(IServiceProvider serviceProvider)
{
    var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();

    IdentityResult roleResult;

    var roleCheck = await RoleManager.RoleExistsAsync("SEOAdmin");
    if (!roleCheck)
    {
        roleResult = await RoleManager.CreateAsync(new IdentityRole("SEOAdmin"));
    }

    roleCheck = await RoleManager.RoleExistsAsync("SEOManager");
    if (!roleCheck)
    {
        roleResult = await RoleManager.CreateAsync(new IdentityRole("SEOManager"));
    }

}

Then, added

CreateUserRoles(serviceProvider).Wait();

To Configure method in Startup.cs, ran once and the roles were set.

Assigning Roles to Users

Then, I wanted something I could see existing users and decide their role before they got access.  The end result looks like this:

To do this, I added a ManageUsersController and retrieved Users and Roles so that I could assign them a role.  I created a the User Role View Model and User View Model to reflect the drop downs and added a corresponding view.

That was the last step to getting this little SEO backend together.

All packaged together, the entire project can all be seen on Github.

 

Continue Reading