Add Columns/Properties to AspNetUserLogins/Logins

2019-07-12 03:15发布

Is it possible to add columns to the AspNetUserLogins table, or subclass the IdentityUserLogin class, such that the Identity Framework will use that class properly?

1条回答
我命由我不由天
2楼-- · 2019-07-12 03:35

This is an answer but I'm sure it's not going to end up the best one:

It can be done, but it's ugly.

First, you'll want to make a class of all the generics you're about to use, just to make your life easier. Those are:

[Table("AspNetUserRoles")]
public class StandardUserRole : IdentityUserRole<string>

[Table("AspNetRoles")]
public class StandardRole : IdentityRole<string, StandardUserRole>

[Table("AspNetUserLogins")]
public class LoginIdentity : IdentityUserLogin

(The above superclasses can be found in Microsoft.AspNet.Identity.EntityFramework).

This is going to make the following generic definitions shorter, and harder to get into a place where they won't compile due to clerical errors.

While you're here may as well add these to the DbContext, which normally does not leave them available to you:

public DbSet<LoginIdentity> LoginIdentities { get; set; }
public DbSet<StandardUserRole> UserRoles { get; set; }

Now, here comes the crazy:

public class Db :
    // Replace this with a custom implementation
    //IdentityDbContext<Visitor>,
    IdentityDbContext<Visitor, StandardRole, string, LoginIdentity,
        StandardUserRole, IdentityUserClaim>,

And, Visitor is going to need its own adjustment to match this declaration:

public class Visitor : IdentityUser<string, LoginIdentity, StandardUserRole,
    IdentityUserClaim>

That satisfies the Models (which btw, are best to have in their own Project for Migrations performance reasons). But, you've still got all the Identity/OWIN stuff to deal with.

By default you're provided with an ApplicationUserManager that involves a UserStore. It normally inherits from UserManager, but that's going to be too restrictive now - you need to slightly expand it:

public class VisitorManager : UserManager<Visitor, string>
{
    public VisitorManager(IUserStore<Visitor, string> store)
        : base(store)

    {
    }

    public static VisitorManager Create(
        IdentityFactoryOptions<VisitorManager> options,
        IOwinContext context) 
    {
        var manager = new VisitorManager(new UserStore<Visitor,
            StandardRole, string, LoginIdentity, StandardUserRole,
            IdentityUserClaim>(context.Get<Db>()));

I warned you about crazy. SignInManager:

public class SignInManager : SignInManager<Visitor, string>
{
    public SignInManager(VisitorManager userManager,
        IAuthenticationManager authenticationManager)
        : base(userManager, authenticationManager)
    {
    }

    public override Task<ClaimsIdentity> CreateUserIdentityAsync(
        Visitor user)
    {
        return user.GenerateUserIdentityAsync((VisitorManager)UserManager);
    }

    public static SignInManager Create(
        IdentityFactoryOptions<SignInManager> options, IOwinContext context)
    {
        return new SignInManager(context.GetUserManager<VisitorManager>(),
            context.Authentication);
    }
}

That should get you through most of the dirty work. Not easy. But, having done that, you've got a working implementation where you can add extra fields to the Logins table! You can now extend the OWIN Auth stuff to provide events, and listen for the creation of new Logins. You can then respond to those by adding that extra info.

In our case, the goal was to have multiple Logins from multiple OpenId/OAuth Providers (Google, Facebook, etc) across multiple email addresses, on a single User/Visitor account. The framework does support that, but, it doesn't make a record of what Email is associated with what Login row, which is important when merging more Logins with a given account.

[Table("AspNetUserLogins")]
public class LoginIdentity : IdentityUserLogin
{
    /// <summary>
    /// The email address associated with this identity at this provider
    /// </summary>
    [MaxLength(300)]
    public string Email { get; set; }
}

There's more you'll need to do to get the whole thing working, but it should be relatively obvious from the above starting point - with one exception, which I'll point out here.

By migrating from UserManager<TVisitor> to UserManager<TVisitor, string>, you quietly lose the ID-generation functionality built-in to the former. You'll need to emulate it yourself. As another gotcha, along the way you'll most likely implement Visitor as IUser<string>. Doing so will prevent you from setting the Id property, because it's read-only (no setter). You can avoid that with a second interface:

public interface IVisitor
{
    string Id { get; set; }
    string Uid { get; set; }
    string UserName { get; set; }
    string Email { get; set; }
    string FirstName { get; set; }
    string LastName { get; set; }
    ICollection<StandardUserRole> Roles { get; }
    ICollection<LoginIdentity> Logins { get; }
}

With that in place you can set Id safely (even in an abstracted class):

public override Task<IdentityResult> CreateAsync(Visitor user)
{
    var guid = Guid.NewGuid();
    string id = guid.ToString();
    ((IVisitor)user).Id = id;
    return base.CreateAsync(user);
}

Remember to do same for CreateAsync(Visitor user, string password). Otherwise created users explode with DbEntityValidationException complaining Id is a required field.

查看更多
登录 后发表回答