19

I am migrating an ASP.NET Core 1.0 application to ASP.NET Core 2.0.

In my startup I am configuring two identities:

services.AddIdentity<IdentityUser, IdentityRole>(configureIdentity)
   .AddDefaultTokenProviders()
   .AddUserStore<IdentityUserStore<IdentityUser>>()
   .AddRoleStore<IdentityRoleStore<IdentityRole>>();

services.AddIdentity<Customer, CustomerRole>(configureIdentity)
   .AddDefaultTokenProviders()
   .AddErrorDescriber<CustomerIdentityErrorDescriber>()
   .AddUserStore<CustomerStore<Customer>>()
   .AddRoleStore<CustomerRoleStore<CustomerRole>>();

This worked fine in ASP.NET Core 1.0 but fails with the error: System.InvalidOperationException: 'Scheme already exists: Identity.Application' in ASP.NET Core 2.0.

In ASP.NET Core 2.0, if I remove one of the calls to AddIdentity the error goes away. How do I migrate my code so that I can use two different types of identity user and role in my application? Or did I just make a fundamental error in understanding how things work back when I wrote this in ASP.NET Core 1.0?

SpruceMoose
  • 9,737
  • 4
  • 39
  • 53
keith
  • 5,122
  • 3
  • 21
  • 50

3 Answers3

24

After looking through the ASP.NET Core source code on github, a second identity could be added using this extension method:

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Collections.Generic;
using System.Text;

namespace Whatever
{
    public static class IdentityExtensions
    {
        public static IdentityBuilder AddSecondIdentity<TUser, TRole>(
            this IServiceCollection services)
            where TUser : class
            where TRole : class
        {
            services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
            services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
            services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
            services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
            services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
            services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
            services.TryAddScoped<UserManager<TUser>, AspNetUserManager<TUser>>();
            services.TryAddScoped<SignInManager<TUser>, SignInManager<TUser>>();
            services.TryAddScoped<RoleManager<TRole>, AspNetRoleManager<TRole>>();

            return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
        }
    }
}
keith
  • 5,122
  • 3
  • 21
  • 50
  • It really helped much. But why do we had to add Create a builder method; `AddSecondIdentity`? – KaraKaplanKhan May 11 '18 at 07:47
  • @ShiroiTora, I'm not sure I fully understand the question: do you mean *is it necessary to use an extension method?* or do you mean *why did Microsoft change the design in this way?* – keith May 11 '18 at 08:03
  • I will go with first question. In startup, I used `services.AddIdentity<..>` for my first Identity then used `services.AddSecondIdentity<..>` for my second Identity. Why do we needed this extension method? – KaraKaplanKhan May 11 '18 at 08:09
  • 2
    @ShiroiTora, it doesn't need to be an extension method, but Microsoft changed the design of the identity provider in .NET Core 2.0 so that `AddIdentity` could not be used to add further identities. I think their design choice means that a single identity should be used and you should handle multiple sources of identities in a single provider. The extension method I've written simply replicates the way .NET Core 1.0 behaved i.e. it is lifted from the source code for .NET Core. – keith May 11 '18 at 08:18
  • I understood completely now. Thanks. – KaraKaplanKhan May 11 '18 at 09:51
  • @keith may i ask how to use it? – Joebet Mutia Apr 19 '19 at 13:10
  • @JoebetMutia, it was designed to port code from .NET core 1. If you are already using a second identity in .NET core 1, then this is a drop in replacement, except in my original example the second call is `AddSecondIdentity` rather than `AddIdentity`. – keith Jul 03 '19 at 10:28
  • @ZyxSun nope.. i decided to create a web api call from a project that uses that identity db. keith thanks for the reply. – Joebet Mutia Sep 13 '19 at 15:42
  • You should also add this line: services.TryAddScoped, DefaultUserConfirmation>(); – hakantopuz Nov 29 '20 at 19:04
13

Asp.net Core 2.2 provides a built-in method for that purpose.

AddIdentityCore<TUser>

How to use it:

services.AddIdentity<IdentityUser, IdentityRole>(configureIdentity)
   .AddDefaultTokenProviders()
   .AddUserStore<IdentityUserStore<IdentityUser>>()
   .AddRoleStore<IdentityRoleStore<IdentityRole>>();

services.AddIdentityCore<Customer>(configureIdentity)
   .AddDefaultTokenProviders()
   .AddErrorDescriber<CustomerIdentityErrorDescriber>()
   .AddUserStore<CustomerStore<Customer>>()
   .AddRoleStore<CustomerRoleStore<CustomerRole>>();

services.AddScoped<RoleManager<Customer>>();

In fact, read the implementation of this method from asp.net core 2.2 github repo

    /// <summary>
    /// Adds and configures the identity system for the specified User type. Role services are not added by default 
    /// but can be added with <see cref="IdentityBuilder.AddRoles{TRole}"/>.
    /// </summary>
    /// <typeparam name="TUser">The type representing a User in the system.</typeparam>
    /// <param name="services">The services available in the application.</param>
    /// <param name="setupAction">An action to configure the <see cref="IdentityOptions"/>.</param>
    /// <returns>An <see cref="IdentityBuilder"/> for creating and configuring the identity system.</returns>
    public static IdentityBuilder AddIdentityCore<TUser>(this IServiceCollection services, Action<IdentityOptions> setupAction)
        where TUser : class
    {
        // Services identity depends on
        services.AddOptions().AddLogging();

        // Services used by identity
        services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
        services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
        services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
        services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
        services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
        // No interface for the error describer so we can add errors without rev'ing the interface
        services.TryAddScoped<IdentityErrorDescriber>();
        services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();
        services.TryAddScoped<UserManager<TUser>>();

        if (setupAction != null)
        {
            services.Configure(setupAction);
        }

        return new IdentityBuilder(typeof(TUser), services);
    }
cyberdantes
  • 1,342
  • 3
  • 16
  • 28
1

Thank you very much for your answer keith. This saved me a lot of time! One small improvement: I had to configure some options (IdentityOptions) in my case. Like for example: Password Complexity Rules.

I therefore included the registering of the Action setupAction. (This is the same way Microsoft does it within the AddIdentity inside IdentityServiceCollectionExtension)

public static class IdentityExtensions
{
    public static IdentityBuilder AddSecondIdentity<TUser, TRole>(
        this IServiceCollection services, Action<IdentityOptions> setupAction)
        where TUser : class
        where TRole : class
    {
        services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
        services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
        services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
        services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
        services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
        services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
        services.TryAddScoped<UserManager<TUser>, AspNetUserManager<TUser>>();
        services.TryAddScoped<SignInManager<TUser>, SignInManager<TUser>>();
        services.TryAddScoped<RoleManager<TRole>, AspNetRoleManager<TRole>>();

        if (setupAction != null)
            services.Configure(setupAction);

        return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
    }
}
Riscie
  • 3,775
  • 1
  • 24
  • 31
  • im confused how did you guys used it? currently i have this services.AddDefaultIdentity() .AddEntityFrameworkStores(); services.AddSecondIdentity() .AddEntityFrameworkStores(); but dont know how to initialize those 2 to use – Joebet Mutia Apr 19 '19 at 02:37