Mark Johnson

Software EngineerLucky HusbandProud FatherHopeless NerdAmateur BuilderDisney FanaticNeglectful Blogger

ASP.NET Core Identity Without Entity Framework

When ASP.NET Identity was introduced back in 2013 it represented a huge step forward in extensibility and flexibility over its predecessors. Out of the box, it used Entity Framework to make it super easy to go from “File -> New Project” to a fully functional web application with support for user registration and login.

Of course, Entity Framework isn’t for everyone, and ASP.NET Identity’s extensibility made it easy to remove the Entity Framework integration and replace it with something else.

With the release of ASP.NET Core, Microsoft also released a refreshed version of ASP.NET Identity called — wait for it — ASP.NET Core Identity. Like its predecessor it also uses Entity Framework out of the box, and also like its predecessor, its extensibility makes it easy to replace it.

Show Me the Code

If you can’t wait to get started, all of the code is in a fully functioning sample application here: https://github.com/mark-j/dapper-identity

The files you’ll want to pay particular attention to are:

  • Database/ApplicationUser.sql
  • Database/ApplicationRole.sql
  • Database/ApplicationUserRole.sql
  • WebApp/Models/ApplicationUser.cs
  • WebApp/Models/ApplicationRole.cs
  • WebApp/Data/UserStore.cs
  • WebApp/Data/RoleStore.cs
  • WebApp/Startup.cs (Specifically the first three lines inย ConfigureServices())

Database

You can use any database or custom repository layer you’d like, but for the purposes of this post I’ll use SQL Server. Let’s start by creating a quick table to hold our user data:

CREATE TABLE [dbo].[ApplicationUser]
(
    [Id] INT NOT NULL PRIMARY KEY IDENTITY,
    [UserName] NVARCHAR(256) NOT NULL,
    [NormalizedUserName] NVARCHAR(256) NOT NULL,
    [Email] NVARCHAR(256) NULL,
    [NormalizedEmail] NVARCHAR(256) NULL,
    [EmailConfirmed] BIT NOT NULL,
    [PasswordHash] NVARCHAR(MAX) NULL,
    [PhoneNumber] NVARCHAR(50) NULL,
    [PhoneNumberConfirmed] BIT NOT NULL,
    [TwoFactorEnabled] BIT NOT NULL
)

GO

CREATE INDEX [IX_ApplicationUser_NormalizedUserName] ON [dbo].[ApplicationUser] ([NormalizedUserName])

GO

CREATE INDEX [IX_ApplicationUser_NormalizedEmail] ON [dbo].[ApplicationUser] ([NormalizedEmail])

And another one for role data:

CREATE TABLE [dbo].[ApplicationRole]
(
    [Id] INT NOT NULL PRIMARY KEY IDENTITY,
    [Name] NVARCHAR(256) NOT NULL,
    [NormalizedName] NVARCHAR(256) NOT NULL
)

GO

CREATE INDEX [IX_ApplicationRole_NormalizedName] ON [dbo].[ApplicationRole] ([NormalizedName])

Pretty basic stuff. Note that I added some indexes as well because I’ll need to look users/roles up by those columns pretty often.

Models

Next, we need some models to work with this data up in the application layer. Just some plain old objects will do:

public class ApplicationUser
{
    public int Id { get; set; }

    public string UserName { get; set; }

    public string NormalizedUserName { get; set; }

    public string Email { get; set; }

    public string NormalizedEmail { get; set; }

    public bool EmailConfirmed { get; set; }

    public string PasswordHash { get; set; }

    public string PhoneNumber { get; set; }

    public bool PhoneNumberConfirmed { get; set; }

    public bool TwoFactorEnabled { get; set; }
}

And for the roles:

public class ApplicationRole
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string NormalizedName { get; set; }
}

Again, pretty basic. Remember, these are just examples. Your objects can look like anything.

Data Stores

Since our database and data objects can be just about anything, Identity needs us to implement some interfaces for its data storage needs. Which interfaces we need to implement depends on what features we want to use. ASP.NET Core Identity supports all sorts of fancy things like 2FA and external login providers. For this example I picked the ones that give us most of the basic functionality.

These classes can be anywhere, and use any implementation. You might want to have them call out to a repository layer or custom data access code. For brevity I decided to use Dapper to access the SQL database right there in the classes:

public class UserStore : IUserStore<ApplicationUser>, IUserEmailStore<ApplicationUser>, IUserPhoneNumberStore<ApplicationUser>,
    IUserTwoFactorStore<ApplicationUser>, IUserPasswordStore<ApplicationUser>
{
    private readonly string _connectionString;

    public UserStore(IConfiguration configuration)
    {
        _connectionString = configuration.GetConnectionString("DefaultConnection");
    }

    public async Task<IdentityResult> CreateAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync(cancellationToken);
            user.Id = await connection.QuerySingleAsync<int>([email protected]"INSERT INTO [ApplicationUser] ([UserName], [NormalizedUserName], [Email],
                [NormalizedEmail], [EmailConfirmed], [PasswordHash], [PhoneNumber], [PhoneNumberConfirmed], [TwoFactorEnabled])
                VALUES (@{nameof(ApplicationUser.UserName)}, @{nameof(ApplicationUser.NormalizedUserName)}, @{nameof(ApplicationUser.Email)},
                @{nameof(ApplicationUser.NormalizedEmail)}, @{nameof(ApplicationUser.EmailConfirmed)}, @{nameof(ApplicationUser.PasswordHash)},
                @{nameof(ApplicationUser.PhoneNumber)}, @{nameof(ApplicationUser.PhoneNumberConfirmed)}, @{nameof(ApplicationUser.TwoFactorEnabled)});
                SELECT CAST(SCOPE_IDENTITY() as int)", user);
        }

        return IdentityResult.Success;
    }

    public async Task<IdentityResult> DeleteAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync(cancellationToken);
            await connection.ExecuteAsync($"DELETE FROM [ApplicationUser] WHERE [Id] = @{nameof(ApplicationUser.Id)}", user);
        }

        return IdentityResult.Success;
    }

    public async Task<ApplicationUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync(cancellationToken);
            return await connection.QuerySingleOrDefaultAsync<ApplicationUser>([email protected]"SELECT * FROM [ApplicationUser]
                WHERE [Id] = @{nameof(userId)}", new { userId });
        }
    }

    public async Task<ApplicationUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync(cancellationToken);
            return await connection.QuerySingleOrDefaultAsync<ApplicationUser>([email protected]"SELECT * FROM [ApplicationUser]
                WHERE [NormalizedUserName] = @{nameof(normalizedUserName)}", new { normalizedUserName });
        }
    }

    public Task<string> GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        return Task.FromResult(user.NormalizedUserName);
    }

    public Task<string> GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        return Task.FromResult(user.Id.ToString());
    }

    public Task<string> GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        return Task.FromResult(user.UserName);
    }

    public Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, CancellationToken cancellationToken)
    {
        user.NormalizedUserName = normalizedName;
        return Task.FromResult(0);
    }

    public Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken)
    {
        user.UserName = userName;
        return Task.FromResult(0);
    }

    public async Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync(cancellationToken);
            await connection.ExecuteAsync([email protected]"UPDATE [ApplicationUser] SET
                [UserName] = @{nameof(ApplicationUser.UserName)},
                [NormalizedUserName] = @{nameof(ApplicationUser.NormalizedUserName)},
                [Email] = @{nameof(ApplicationUser.Email)},
                [NormalizedEmail] = @{nameof(ApplicationUser.NormalizedEmail)},
                [EmailConfirmed] = @{nameof(ApplicationUser.EmailConfirmed)},
                [PasswordHash] = @{nameof(ApplicationUser.PasswordHash)},
                [PhoneNumber] = @{nameof(ApplicationUser.PhoneNumber)},
                [PhoneNumberConfirmed] = @{nameof(ApplicationUser.PhoneNumberConfirmed)},
                [TwoFactorEnabled] = @{nameof(ApplicationUser.TwoFactorEnabled)}
                WHERE [Id] = @{nameof(ApplicationUser.Id)}", user);
        }

        return IdentityResult.Success;
    }

    public Task SetEmailAsync(ApplicationUser user, string email, CancellationToken cancellationToken)
    {
        user.Email = email;
        return Task.FromResult(0);
    }

    public Task<string> GetEmailAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        return Task.FromResult(user.Email);
    }

    public Task<bool> GetEmailConfirmedAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        return Task.FromResult(user.EmailConfirmed);
    }

    public Task SetEmailConfirmedAsync(ApplicationUser user, bool confirmed, CancellationToken cancellationToken)
    {
        user.EmailConfirmed = confirmed;
        return Task.FromResult(0);
    }

    public async Task<ApplicationUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync(cancellationToken);
            return await connection.QuerySingleOrDefaultAsync<ApplicationUser>([email protected]"SELECT * FROM [ApplicationUser]
                WHERE [NormalizedEmail] = @{nameof(normalizedEmail)}", new { normalizedEmail });
        }
    }

    public Task<string> GetNormalizedEmailAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        return Task.FromResult(user.NormalizedEmail);
    }

    public Task SetNormalizedEmailAsync(ApplicationUser user, string normalizedEmail, CancellationToken cancellationToken)
    {
        user.NormalizedEmail = normalizedEmail;
        return Task.FromResult(0);
    }

    public Task SetPhoneNumberAsync(ApplicationUser user, string phoneNumber, CancellationToken cancellationToken)
    {
        user.PhoneNumber = phoneNumber;
        return Task.FromResult(0);
    }

    public Task<string> GetPhoneNumberAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        return Task.FromResult(user.PhoneNumber);
    }

    public Task<bool> GetPhoneNumberConfirmedAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        return Task.FromResult(user.PhoneNumberConfirmed);
    }

    public Task SetPhoneNumberConfirmedAsync(ApplicationUser user, bool confirmed, CancellationToken cancellationToken)
    {
        user.PhoneNumberConfirmed = confirmed;
        return Task.FromResult(0);
    }

    public Task SetTwoFactorEnabledAsync(ApplicationUser user, bool enabled, CancellationToken cancellationToken)
    {
        user.TwoFactorEnabled = enabled;
        return Task.FromResult(0);
    }

    public Task<bool> GetTwoFactorEnabledAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        return Task.FromResult(user.TwoFactorEnabled);
    }

    public Task SetPasswordHashAsync(ApplicationUser user, string passwordHash, CancellationToken cancellationToken)
    {
        user.PasswordHash = passwordHash;
        return Task.FromResult(0);
    }

    public Task<string> GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        return Task.FromResult(user.PasswordHash);
    }

    public Task<bool> HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        return Task.FromResult(user.PasswordHash != null);
    }

    public void Dispose()
    {
        // Nothing to dispose.
    }
}

And another one for the roles:

public class RoleStore : IRoleStore<ApplicationRole>
{
    private readonly string _connectionString;

    public RoleStore(IConfiguration configuration)
    {
        _connectionString = configuration.GetConnectionString("DefaultConnection");
    }

    public async Task<IdentityResult> CreateAsync(ApplicationRole role, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync(cancellationToken);
            role.Id = await connection.QuerySingleAsync<int>([email protected]"INSERT INTO [ApplicationRole] ([Name], [NormalizedName])
                VALUES (@{nameof(ApplicationRole.Name)}, @{nameof(ApplicationRole.NormalizedName)});
                SELECT CAST(SCOPE_IDENTITY() as int)", role);
        }

        return IdentityResult.Success;
    }

    public async Task<IdentityResult> UpdateAsync(ApplicationRole role, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync(cancellationToken);
            await connection.ExecuteAsync([email protected]"UPDATE [ApplicationRole] SET
                [Name] = @{nameof(ApplicationRole.Name)},
                [NormalizedName] = @{nameof(ApplicationRole.NormalizedName)}
                WHERE [Id] = @{nameof(ApplicationRole.Id)}", role);
        }

        return IdentityResult.Success;
    }

    public async Task<IdentityResult> DeleteAsync(ApplicationRole role, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync(cancellationToken);
            await connection.ExecuteAsync($"DELETE FROM [ApplicationRole] WHERE [Id] = @{nameof(ApplicationRole.Id)}", role);
        }

        return IdentityResult.Success;
    }

    public Task<string> GetRoleIdAsync(ApplicationRole role, CancellationToken cancellationToken)
    {
        return Task.FromResult(role.Id.ToString());
    }

    public Task<string> GetRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken)
    {
        return Task.FromResult(role.Name);
    }

    public Task SetRoleNameAsync(ApplicationRole role, string roleName, CancellationToken cancellationToken)
    {
        role.Name = roleName;
        return Task.FromResult(0);
    }

    public Task<string> GetNormalizedRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken)
    {
        return Task.FromResult(role.NormalizedName);
    }

    public Task SetNormalizedRoleNameAsync(ApplicationRole role, string normalizedName, CancellationToken cancellationToken)
    {
        role.NormalizedName = normalizedName;
        return Task.FromResult(0);
    }

    public async Task<ApplicationRole> FindByIdAsync(string roleId, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync(cancellationToken);
            return await connection.QuerySingleOrDefaultAsync<ApplicationRole>([email protected]"SELECT * FROM [ApplicationRole]
                WHERE [Id] = @{nameof(roleId)}", new { roleId });
        }
    }

    public async Task<ApplicationRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync(cancellationToken);
            return await connection.QuerySingleOrDefaultAsync<ApplicationRole>([email protected]"SELECT * FROM [ApplicationRole]
                WHERE [NormalizedName] = @{nameof(normalizedRoleName)}", new { normalizedRoleName });
        }
    }

    public void Dispose()
    {
        // Nothing to dispose.
    }
}

Wireup

Finally, we need to tell Identity to use our custom data stores instead of Entity Framework. Like most of ASP.NET Core, this is done through IOC.

In Startup.cs we simply remove the Entity Framework stuff and register our custom classes instead:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IUserStore<ApplicationUser>, UserStore>();
    services.AddTransient<IRoleStore<ApplicationRole>, RoleStore>();

    services.AddIdentity<ApplicationUser, ApplicationRole>()
        .AddDefaultTokenProviders();

    // Add application services.
    services.AddTransient<IEmailSender, EmailSender>();

    services.AddMvc();
}

20 Comments

  • Why you don’t setup the UserRole table/functionality?

    • Hi Marco. That’s a great observation. I only set up enough support in this example for what I think is the most common stuff — but UserRole functionality is pretty common. I’ll add it!

      • So despite using async/await, all of the methods that you have here are *not asynchronous*? That is to say, they are all completely synchronous and blocking?
        I’m asking because I would like to do exactly what you’ve done here but I do not, at this point, use async methods.

        • It’s a bit of a mix. In some cases (specifically in methods with the “async” modifier) they are asynchronous and will not block. Those will return a Task object that will complete some time later.

          Other methods will still return Task objects, but they will already be in a complete state when they return.

          To conform to the contract enforced by the interfaces, you need to return Task objects in most cases, but they do not require you to do anything async. You can totally write your methods like a normal, blocking method. If you use Task.FromResult(…) you can synchronously return your result at the end of the method.

    • Hi Marco, have you been able to add UserRole? I need it too.

    • Fixed! Just pushed an update to the GitHub repo.

  • Hello Mark, I do not speak English, so I’m using google translator.
    I did the login part the way you showed it, now I need the roles.
    You would help me greatly if you add UserRole functionality.
    Can you send me an example of how to do this?
    Later I will need to add claims as well.

  • Great article Mark, It helped me connect identity with mysql. Need to setup external logins and the UserStore is missing the implementation for IUserLoginStore. Can you please set that up as well?

  • Hi Mark,

    Thanks for the awesome article. I was wondering if you could possibly elaborate on how would you setup Claims with this? For example, we may have a couple of fields that are in the user’s table, or even outside of it that we would like to add to Identity claims so that we can retrieve them through something like User.Identity.GetMyCustomField() option. This is how things were done in MVC 5 and EF6. Is this possible with core and dapper?

  • yea, so.. if you ever be in poland, i would buy you a beer ๐Ÿ˜‰

  • hi
    Thank you so much for this example
    but, there is a problem in Two Factor and gives Error
    Please help resolve the error

    thanks again

  • Thanks for the sample, was really helpful in ditching EF.

  • Why use Dapper? If doing it without Entity Framework, the point is to do it without an ORM. All you’re doing is trading one ORM for another, so might as well use Entity Framework.

  • Hi Mark,

    I’ve get the sample from github, In Two factor enable part, below method is not included in Userstore.cs
    GetAuthenticatorKeyAsync(). due to this below error thrown
    NotSupportedException: Store does not implement IUserAuthenticatorKeyStore.

    can you please guide me how to create it?

    Thanks,
    Saravanan

    • Hi Mark,

      Can you please check on above question
      on create custom methods for two factor verifications.
      for e.g

      var model = new TwoFactorAuthenticationViewModel
      {
      HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
      Is2faEnabled = user.TwoFactorEnabled,
      RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user)
      };

      No custom method for GetAuthenticatorKeyAsync(user). Can you help me to create cusom method for generate authKey to be validate on google/Microsoft authenticator.

      Thanks,
      Saravanan

  • How do you set the roles for the user and then authenticate them on secured controllers? I can use the Authorize decorator fine, but not Authorize(Roles=”RolesHer”)


Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.