20

In an ASP.NET Core application, I want to create certain roles as a basis to manage different user-permissions. Sadly, the documentation inform detailled how to use custom roles e.g. in controllers/actions, but not how to create them. I found out that I can use RoleManager<IdentityRole> for this, where the instance gets automatically injected in a controller-constructor, when its defined and ASP.NET Core identity is registered in the application.

This let me add a custom role like this:

var testRole = new IdentityRole("TestRole");
if(!roleManager.RoleExistsAsync(testRole.Name).Result) {
    roleManager.CreateAsync(testRole);
}

It works and create the role in the database. But this check will always create overhead on the database, calling the specific controller/action. So I want to check once after my application has started, if the custom role exists and add them. The ConfigureServices method in Startup.cs seems good for this.

But: How can I create a instance of the RoleManager<IdentityRole> class for doing this? I would like to use a best practice approach here and not messing around by creating depending instances on my own, which seems to cause a lot of work since its not good documentated and will surely not follow best practices, since ASP.NET Core is using dependency injection for things like this (which is also reasonable in my oppinion).

In other words: I need to use dependeny injection outside of a controller.

tmg
  • 19,895
  • 5
  • 72
  • 76
Lion
  • 16,606
  • 23
  • 86
  • 148
  • you might be interested in my project which provides management for Identity users, roles, and claims https://github.com/joeaudette/cloudscribe – Joe Audette Oct 08 '16 at 15:52

3 Answers3

28

Here is an example for your needs that migrates and seeds the database on startup:

Create a static class:

public static class RolesData
{
    private static readonly string[] Roles = new string[] {"Administrator", "Editor", "Subscriber"};

    public static async Task SeedRoles(IServiceProvider serviceProvider)
    {
        using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
        {
            var dbContext = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();

            if (dbContext.Database.GetPendingMigrations().Any())
            {
                await dbContext.Database.MigrateAsync();

                var roleManager = serviceScope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();

                foreach (var role in Roles)
                {
                    if (!await roleManager.RoleExistsAsync(role))
                    {
                        await roleManager.CreateAsync(new IdentityRole(role));
                    }
                }
            }
        }
    }
}

And in Startup.cs:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...

    RolesData.SeedRoles(app.ApplicationServices).Wait();
}
poke
  • 369,085
  • 72
  • 557
  • 602
tmg
  • 19,895
  • 5
  • 72
  • 76
  • 1
    If you are using migrations, the `if (await db.Database.EnsureCreatedAsync())` condition is `false` so no roles are created. – Artholl Jan 23 '17 at 12:18
  • @Artholl Good catch. I replace with `if (dbContext.Database.GetPendingMigrations().Any())` and `await dbContext.Database.MigrateAsync();` – tmg Jan 23 '17 at 21:46
  • Try do the IF only if migrations are running... Something like `if (dbContext.Database.GetPendingMigrations().Any()) await dbContext.Database.MigrateAsync();` – Yuki Mar 18 '17 at 14:30
  • 1
    When I use `dbContext.Database.GetPendingMigrations().Any()` I'm getting a `'DatabaseFacade' does not contain a definition for 'GetPendingMigrations'` – Nick May 11 '17 at 16:27
  • @Nick I think you are missing "using Microsoft.EntityFrameworkCore"? – tmg May 11 '17 at 16:38
  • @Nick Entity Framework Core is a new project. Many things changed. [Compare EF Core & EF6.x](https://learn.microsoft.com/en-us/ef/efcore-and-ef6/) – tmg May 11 '17 at 16:49
  • I just checked and I have "using Microsoft.EntityFrameworkCore" with "using Microsoft.EntityFrameworkCore;" Maybe I cannot use both? – Nick May 11 '17 at 16:51
  • @Nick same namespace twice or there is a typo here? – tmg May 11 '17 at 16:58
  • @tmg Typo, sorry. It's "using Microsoft.EntityFrameworkCore" and "using Microsoft.AspNetCore.Identity;" – Nick May 11 '17 at 17:01
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/143995/discussion-between-nick-and-tmg). – Nick May 11 '17 at 17:01
  • This does not work for me in ASP.NET MVC Core 1.1 in VS 2017. The roles are not created. – tnktnk May 17 '17 at 19:00
  • @tnktnk I updated to ASP.NET Core 1.1.2 (VS 2017) and it works. Are you sure that the issue comes from this block of code? – tmg May 17 '17 at 19:58
  • 1
    @tmg, the code wasn't causing a problem but it wasn't doing what it is supposed to do either. Why tie the creation of seed roles to a pending migration check? WHy not just select the roles and see if it exists and then if not, add it. – tnktnk May 19 '17 at 05:11
  • @tmg Do you know if there's a change to be done for .Net Core 2.0? I'm trying to execute but always get an error on this line: `var roleManager = serviceProvider.GetRequiredService>();` – Mario Duarte Aug 21 '17 at 02:37
  • 1
    @HenriqueDuarte The code was trying to resolve an instance of `RoleManager` from wrong scope. I updated the answer to fix that. – tmg Aug 21 '17 at 16:54
  • Can you update your answer according to .NET framework 4.6? – ninbit Oct 22 '19 at 15:19
6

I would prefer to seed the data only if there are no roles already inserted in the database. In other word store the roles only when the application is running for the first time:

public static class RolesData
{
    private static readonly string[] Roles = new string[] { "Administrator", "Editor", "Subscriber" };

    public static async Task SeedRoles(IServiceProvider serviceProvider)
    {
        using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
        {
            var dbContext = serviceScope.ServiceProvider.GetService<AppDbContext>();

            if (!dbContext.UserRoles.Any())
            {
                var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();

                foreach (var role in Roles)
                {
                    if (!await roleManager.RoleExistsAsync(role))
                    {
                        await roleManager.CreateAsync(new IdentityRole(role));
                    }
                }
            }

        }
    }
}

And on Startup.cs:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...

    RolesData.SeedRoles(app.ApplicationServices).Wait();
}
Millan Sanchez
  • 154
  • 2
  • 8
0

Just in case anyone comes here looking for a .NET 5 solution to this, here it is:

Everything is almost the same as @Millan Sanchez's answer, apart from the bit in the Configure method...

I was getting

One or more errors occurred. (Cannot resolve scoped service 'Microsoft.AspNetCore.Identity.RoleManager`1[Microsoft.AspNetCore.Identity.IdemtityRole]' from root provider.)' 

After another bit of searching I came across this StackOverflow answer

Essentially, all I needed to do was add in IServiceProvider into the parameters of Configure method, and pass that into my call to SeedUsers, instead of app.applicationServices

Full Code:

public static class RolesData
{
    private static readonly string[] Roles = new string[] { "Administrator", "Editor", "Subscriber" };

    public static async Task SeedRoles(IServiceProvider serviceProvider)
    {
        using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
        {
            var dbContext = serviceScope.ServiceProvider.GetService<AppDbContext>();

            if (!dbContext.UserRoles.Any())
            {
                var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();

                foreach (var role in Roles)
                {
                    if (!await roleManager.RoleExistsAsync(role))
                    {
                        await roleManager.CreateAsync(new IdentityRole(role));
                    }
                }
            }

        }
    }
}

And on Startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, 
IServiceProvider serviceProvider)
{
    ...

    RolesData.SeedRoles(serviceProvider).Wait();
}
Chris
  • 598
  • 8
  • 23
  • For anyone who might have the same problem I still got an error because of the 'using() {}' (The error came because the using-statement tried to dispose the IServiceProvider) – Niels Feb 26 '22 at 16:56