How to change type of id in Microsoft.AspNet.Identity.EntityFramework.IdentityUser

47,523

Solution 1

So if you want int ids, you need to create your own POCO IUser class and implement your IUserStore for your custom IUser class in the 1.0 RTM release.

This is something we didn't have time to support, but I'm looking into making this easy(ier) in 1.1 right now. Hopefully something will be available in the nightly builds soon.

Updated with 1.1-alpha1 example: How to get nightly builts

If you update to the latest nightly bits, you can try out the new 1.1-alpha1 apis which should make this easier now: Here's what plugging in Guids instead of strings should look like for example

    public class GuidRole : IdentityRole<Guid, GuidUserRole> { 
        public GuidRole() {
            Id = Guid.NewGuid();
        }
        public GuidRole(string name) : this() { Name = name; }
    }
    public class GuidUserRole : IdentityUserRole<Guid> { }
    public class GuidUserClaim : IdentityUserClaim<Guid> { }
    public class GuidUserLogin : IdentityUserLogin<Guid> { }

    public class GuidUser : IdentityUser<Guid, GuidUserLogin, GuidUserRole, GuidUserClaim> {
        public GuidUser() {
            Id = Guid.NewGuid();
        }
        public GuidUser(string name) : this() { UserName = name; }
    }

    private class GuidUserContext : IdentityDbContext<GuidUser, GuidRole, Guid, GuidUserLogin, GuidUserRole, GuidUserClaim> { }
    private class GuidUserStore : UserStore<GuidUser, GuidRole, Guid, GuidUserLogin, GuidUserRole, GuidUserClaim> {
        public GuidUserStore(DbContext context)
            : base(context) {
        }
    }
    private class GuidRoleStore : RoleStore<GuidRole, Guid, GuidUserRole> {
        public GuidRoleStore(DbContext context)
            : base(context) {
        }
    }

    [TestMethod]
    public async Task CustomUserGuidKeyTest() {
        var manager = new UserManager<GuidUser, Guid>(new GuidUserStore(new GuidUserContext()));
        GuidUser[] users = {
            new GuidUser() { UserName = "test" },
            new GuidUser() { UserName = "test1" }, 
            new GuidUser() { UserName = "test2" },
            new GuidUser() { UserName = "test3" }
            };
        foreach (var user in users) {
            UnitTestHelper.IsSuccess(await manager.CreateAsync(user));
        }
        foreach (var user in users) {
            var u = await manager.FindByIdAsync(user.Id);
            Assert.IsNotNull(u);
            Assert.AreEqual(u.UserName, user.UserName);
        }
    }

Solution 2

Using a Stefan Cebulak's answer and a Ben Foster's great blog article ASP.NET Identity Stripped Bare I have came up with below solution, which I have applied to ASP.NET Identity 2.0 with a generated by Visual Studio 2013 AccountController.

The solution uses an integer as a primary key for users and also allows to get an ID of currently logged in user without making a trip to the database.

Here are the steps, you need to follow:

1. Create custom user-related classes

By default, the AccountController uses classes, which are using string, as a type of a primary key. We need to create below classes, which will use an int instead. I have defined all below classes in one file: AppUser.cs

public class AppUser :
    IdentityUser<int, AppUserLogin, AppUserRole, AppUserClaim>,
    IUser<int>
{

}

public class AppUserLogin : IdentityUserLogin<int> { }

public class AppUserRole : IdentityUserRole<int> { }

public class AppUserClaim : IdentityUserClaim<int> { }

public class AppRole : IdentityRole<int, AppUserRole> { }

It will also be useful, to have a custom ClaimsPrincipal, which will easily expose User's ID

public class AppClaimsPrincipal : ClaimsPrincipal
{
    public AppClaimsPrincipal( ClaimsPrincipal principal ) : base( principal )
    { }

    public int UserId
    {
        get { return int.Parse(this.FindFirst( ClaimTypes.Sid ).Value); }
    }
}

2. Create a custom IdentityDbContext

Our application's database context will extend IdentityDbContext, which implements by default all authentication-related DbSets. Even if DbContext.OnModelCreating is an empty method, I am not sure about the IdentityDbContext.OnModelCreating, so when overriding, remember to call base.OnModelCreating( modelBuilder ) AppDbContext.cs

public class AppDbContext :
    IdentityDbContext<AppUser, AppRole, int, AppUserLogin, AppUserRole, AppUserClaim>
{
    public AppDbContext() : base("DefaultConnection")
    {
        // Here use initializer of your choice
        Database.SetInitializer( new CreateDatabaseIfNotExists<AppDbContext>() );
    }


    // Here you define your own DbSet's



    protected override void OnModelCreating( DbModelBuilder modelBuilder )
    {
        base.OnModelCreating( modelBuilder );

        // Here you can put FluentAPI code or add configuration map's
    }
}

3. Create custom UserStore and UserManager, which will use above

AppUserStore.cs

public interface IAppUserStore : IUserStore<AppUser, int>
{

}

public class AppUserStore :
    UserStore<AppUser, AppRole, int, AppUserLogin, AppUserRole, AppUserClaim>,
    IAppUserStore
{
    public AppUserStore() : base( new AppDbContext() )
    {

    }

    public AppUserStore(AppDbContext context) : base(context)
    {

    }
}

AppUserManager.cs

public class AppUserManager : UserManager<AppUser, int>
{
    public AppUserManager( IAppUserStore store ) : base( store )
    {

    }
}

4. Modify AccountController to use your custom classes

Change all UserManager to AppUserManager, UserStore to AppUserStore etc. Take an example of this constructors:

public AccountController()
    : this( new AppUserManager( new AppUserStore( new AppDbContext() ) ) )
{
}

public AccountController(AppUserManager userManager)
{
    UserManager = userManager;
}

5. Add user's ID as a claim to ClaimIdentity stored in a cookie

In step 1, we have created AppClaimsPrincipal, which exposes UserId taken out of ClaimType.Sid. However, to have this claim available, we need to add it, when logging in the user. In AccountController a SingInAsync method is responsible for logging in. We need to add a line to this method, to add the claim.

private async Task SignInAsync(AppUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
    ClaimsIdentity identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);

    // Extend identity claims
    identity.AddClaim( new Claim( ClaimTypes.Sid, user.Id.ToString() ) );

    AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

6. Create a BaseController with a CurrentUser property

To have an easy access to a currently logged in user's ID in your controllers, create an abstract BaseController, from which your controllers will derive. In the BaseController, create a CurrentUser as follows:

public abstract class BaseController : Controller
{
    public AppClaimsPrincipal CurrentUser
    {
        get { return new AppClaimsPrincipal( ( ClaimsPrincipal )this.User ); }
    }


    public BaseController()
    {

    }
}

7. Inherit your controllers from BaseController and enjoy

From now on, you can use CurrentUser.UserId in your controllers to access an ID of a currently logged in user without trips to the database. You can use it, to query only objects, which belong to the user.

You don't have to take care of auto generation of user primary keys - no surprise, Entity Framework by default uses Identity for integer primary keys, when creating tables.

Warning! Keep in mind, that if you implement it in already released project, for already logged in users ClaimsType.Sid will not exist and FindFirst will return null in AppClaimsPrincipal. You need to either force logout all users or handle this scenario in AppClaimsPrincipal

Solution 3

@HaoKung

I've succeeded to make int id's with your nightly builds. User.Identity.GetUserId() problem is still there, but i just did int.parse() for now.

The biggest suprise was that i did not need to create ID by myself, db was made with identity id and it was somehow automatically set for new users Oo...

Model:

    public class ApplicationUser : IdentityUser<int, IntUserLogin, IntUserRole, IntUserClaim>
{
    public ApplicationUser()
    {
    }
    public ApplicationUser(string name) : this() { UserName = name; }
}

public class ApplicationDbContext : IntUserContext
{
    public ApplicationDbContext()
    {

    }
}

private class IntRole : IdentityRole<int, IntUserRole>
{
    public IntRole()
    {
    }
    public IntRole(string name) : this() { Name = name; }
}
private class IntUserRole : IdentityUserRole<int> { }
private class IntUserClaim : IdentityUserClaim<int> { }
private class IntUserLogin : IdentityUserLogin<int> { }
private class IntUserContext : IdentityDbContext<ApplicationUser, IntRole, int, IntUserLogin, IntUserRole, IntUserClaim>
{
    public IntUserContext()
        : base("DefaultConnection")
    {

    }
}
private class IntUserStore : UserStore<ApplicationUser, IntRole, int, IntUserLogin, IntUserRole, IntUserClaim>
{
    public IntUserStore(DbContext context)
        : base(context)
    {

    }
}
private class IntRoleStore : RoleStore<IntRole, int, IntUserRole>
{
    public IntRoleStore(DbContext context)
        : base(context)
    {
    }
}

Controller:

        public AccountController()
        : this(new UserManager<ApplicationUser, int>(new IntUserStore(new ApplicationDbContext())))
    {
    }

    public AccountController(UserManager<ApplicationUser, int> userManager)
    {
        UserManager = userManager;
    }

    public UserManager<ApplicationUser, int> UserManager { get; private set; }

Hope release build will come soon :D...

P.S. Can't write comments so i did an answer, sorry.

Solution 4

As stated here:

In Visual Studio 2013, the default web application uses a string value for the key for user accounts. ASP.NET Identity enables you to change the type of the key to meet your data requirements. For example, you can change the type of the key from a string to an integer.

This topic on the above link shows how to start with the default web application and change the user account key to an integer. You can use the same modifications to implement any type of key in your project. It shows how to make these changes in the default web application, but you could apply similar modifications to a customized application. It shows the changes needed when working with MVC or Web Forms.

Solution 5

Basically you have to :

-Change the type of the key to int in the Identity user class
-Add customized Identity classes that use int as key
-Change the context class and user manager to use int as key
-Change start-up configuration to use int as key
-Change the AccountController to pass int as key

here is link where all steps are explained to achieve this.

Share:
47,523
BenjiFB
Author by

BenjiFB

Updated on August 22, 2020

Comments

  • BenjiFB
    BenjiFB over 3 years

    (ASP.NET MVC 5, EF6, VS2013)

    I'm trying to figure out how to change the type of the "Id" field from string to int in the type:

    Microsoft.AspNet.Identity.EntityFramework.IdentityUser
    

    in order to have new user accounts be associated with an integer ID rather than a GUID. But it seems like this will be more complicated than simply adding a new Id property with type int in my derived user class. Take a look at this method signature:

    (from Assembly Microsoft.AspNet.Identity.Core.dll)

    public class UserManager<TUser> : IDisposable where TUser : global::Microsoft.AspNet.Identity.IUser
      {
      ...
      public virtual Task<IdentityResult> AddLoginAsync(string userId, UserLoginInfo login);
      ...
      }
    

    So it seems that there are other methods baked into the ASP.NET identity framework which require the userId to be a string. Would I need to reimplement these classes as well?

    An explanation of why I don't want to store GUIDs for ids in the user table:

    -There will be other tables that relate data to the users table via a foreign key. (When users save content on the site.) I see no reason to use the larger field type and spend extra database space with no clear advantages. (I know that there are other posts about using GUIDs vs int ids, but it seems like many suggest that int ids are faster and use less space, which still leaves me wondering.)

    -I plan to expose a restful endpoint to allow users to retrieve data about a particular user. I think:

    /users/123/name
    

    is cleaner than

    /users/{af54c891-69ba-4ddf-8cb6-00d368e58d77}/name
    

    Does anyone know why the ASP.NET team decided to implement IDs this way? Am I being short sighted in trying to change this to an int type? (Perhaps there are benefits I'm missing.)

    Thanks...

    -Ben

  • BenjiFB
    BenjiFB over 10 years
    Thanks for clarifying, Hao. Can you explain why GUIDs were used instead of ints?
  • Hao Kung
    Hao Kung over 10 years
    So we decided on string keys to avoid having to deal with key serialization issues, the EF default implementation could have used ints as the primary key, GUIDs were just an easy way to generate a random unique string key.
  • Piotr Stulinski
    Piotr Stulinski over 10 years
    @Hao - why is the implementation using "string" field in the database instead of uniqueidentifier? is there any ways i can use the sql uniqueidentifier in the database?
  • BenjiFB
    BenjiFB over 10 years
    @Hao, thanks for the example, but can you give an example where you use int32 as the type per the original question? It seems like that would be a bit more complicated because with the GUID example, you just ask for a new GUID, but I imagine that the procedure would be different for an int. Thanks...
  • BenjiFB
    BenjiFB over 10 years
    Also, where can we find the nightly bits?
  • Hao Kung
    Hao Kung over 10 years
    added the link for the nightly to the answer, for int you can just replace guid with int, and EF will autogenerate a key for you, so you can remove the NewGuid.
  • BenjiFB
    BenjiFB over 10 years
    Super! I'll give this a shot. Thanks @HaoKung
  • BenjiFB
    BenjiFB over 10 years
    @HaoKung, this seems to fail for me when trying to use int instead of GUID. One such failure: This code comes from the alpha of Assembly Microsoft.AspNet.Identity.Core.dll: if (model.LoginProvider == LocalLoginProvider) { result = await UserManager.RemovePasswordAsync(User.Identity.GetUserId()); } It still returns a string for the ID. But in the SPA sample, look at this: result = await UserManager.RemovePasswordAsync(User.Identity.GetUserId()); RemovePasswordAsync now expects an integer, but gets a string.
  • BenjiFB
    BenjiFB over 10 years
    Can you let me know when there will be documentation available for this? This is very difficult without documentation. Thanks...
  • Hao Kung
    Hao Kung over 10 years
    So there are two different issues, the user id in the ClaimsIdentity will still be represented as a string in the claim, you will need to turn it into a guid yourself from a string in the template before passing it into the user manager. i.e. new Guid(User.Identity.GetUserId())
  • Hao Kung
    Hao Kung over 10 years
    Or you can write your own extension method User.Identity.GetUserGuid which hides that...
  • dwhite
    dwhite over 10 years
    @BenjiFB, have you had any luck with this? I have a User model that has an integer Id property, and I don't really want to have to change it. Of course, to be fair, I don't really want to use alpha code either, so I'd be interested in a solution based on the RTM, if that is at all possible. I couldn't figure out anything with a custom IUserStore as the IUser interface requires a string Id, thus conflicting with my existing property.
  • BenjiFB
    BenjiFB over 10 years
    @dwhite, no, I was not successful in doing this. I didn't even try without the alpha (not sure if it's possible with the RTM bits), but even with the alpha I found that I was having to cascade a lot of changes so I figured I'd rather at least wait until it was no longer an alpha before I invest in the changes.
  • James Reategui
    James Reategui about 10 years
    @HaoKung - with Identity 2.0 beta1, if we want to use Guid as pkey, do we still have to define a GuidUserStore and a GuidRoleStore? ie. do we need to change your above code in some way?
  • Hao Kung
    Hao Kung about 10 years
    It should be more or less the same for beta as above.
  • Quoter
    Quoter about 10 years
    Stefan, what did you do for the ChallengeResult() and AuthenticationManager.GetExternalLoginInfoAsync(). Both methods are located in AccountController() and both expect the UserId as parameter but as a string. So you can't convert it to int. Did you parse User.Identity.GetUserId() to int as well or not?
  • Stefan Cebulak
    Stefan Cebulak about 10 years
    No, to be honest i didn't even noticed this, but authentication by Google and Facebook is working correctly. I think i won't change it but rather wait for release build. This seems to have someting to do with XSRF protection.
  • Quoter
    Quoter about 10 years
    What do you mean with wait for release build? The newest version of asp.net-identity just got released yesterday?
  • Stefan Cebulak
    Stefan Cebulak about 10 years
    That's a nice suprise :], already done update. String id is still there by default in User.Identity.GetUserId(), i wonder if i need to create my own IdentityExtensions to change this. I will probably wait for examples, don't have time to work on it right now.
  • William
    William almost 10 years
    has anyone actually gotten this to work? I just tried and I am getting a "Sequence contains no matching element" error when updating my database.
  • Stefan Cebulak
    Stefan Cebulak almost 10 years
    @William I'm already using this in one of my projects and it works nice so far, but i was creating new database with this instead of updating an existing one.
  • krzychu
    krzychu almost 10 years
    @William, check out my answer. If you encounter any problems, let me know in comments.
  • Necreaux
    Necreaux almost 9 years
    This text should be cited properly.
  • demo
    demo almost 9 years
    Should i register new Identity models with DI (i use Unity) cause i get error The type IUserStore'2 does not have an accessible constructor. and it seems to be problem with this models?
  • Piotr M
    Piotr M about 8 years
    everything works, except this FindFirst(ClaimTypes.Sid) always give me null, so I got an exception at AppClaimsPrincipal.UserId. Looks like after log in user did not receive new claim (i added your code)
  • krzychu
    krzychu about 8 years
    Are you sure all the code in step 5. is being executed? Also please delete whatever trail your login uses (e.g. cookie) and see if it is set properly after login.
  • Piotr M
    Piotr M about 8 years
    Yes, I am sure. User get's logged with identity having all necessary claims. When I'm looking up for user in the controller, it has only 1 claim (at GetAuthenticationManager().SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity); line, it has 4 claims.
  • Piotr M
    Piotr M about 8 years
    Ok, seems like you can not obtain user id in controller when user is getting login into. After redirect everything works fine!
  • Jack
    Jack over 7 years
    @krzychu I also look at the post on asp.net/identity/overview/extensibility/… but not sure if I have to add custom classes to IdentityModels as mentioned at step "Add customized Identity classes that use the key type" on that page. Because I am creating the project now and currently there is no table created. I mean that can I modify the current classes instead of adding the custom ones?
  • Jack
    Jack over 7 years
    @HaoKung I also look at the post on asp.net/identity/overview/extensibility/… but not sure if I have to add custom classes to IdentityModels as mentioned at step "Add customized Identity classes that use the key type" on that page. Because I am creating the project now and currently there is no table created. I mean that can I modify the current classes instead of adding the custom ones?
  • t.durden
    t.durden over 4 years
    @Necreaux what do you mean "cited properly"? please be clear.
  • Necreaux
    Necreaux over 4 years
    @t.durden that first paragraph was copy/paste from the linked website without attribution and technically plagiarism. Since nobody has done anything since this comment, I went ahead and attempted to fix it.