I'm curious about in which layer I should write hashing part?
In the ASP.NET Core Identity, password hashing (PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations) is taken care by the Identity Manager Layer.

See the Microsoft document Custom storage providers for ASP.NET Core Identity.
Why don't you consider customizing the ASP.NET Core Identity? As mentioned in the above document you will be able to use a different data access approach, such as Dapper.
You will have to create only the data source, the data access layer, and the store classes that interact with this data access layer (the green and grey boxes in the diagram above).
Other than password hashing, the functionalities available in the ASP.NET Core Identity such as issue of authentication cookie, redirect to login page, validation of user name and password, prevention of duplicated user name, persistence and sliding expiration will be provided by the Identity Manager and ASP.NET Core App layers.
Blow is a sample code of UserStore in Identity Store layer. The code uses EF Core. You will be able to rewrite the code to use Dapper instead of EF Core.
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MvcIdCustom.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MvcIdCustom.DAL
{
public class UserStore : IUserStore<User>,
IUserPasswordStore<User>,
IQueryableUserStore<User>
{
private DataContext db;
public UserStore(DataContext db)
{
this.db = db;
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (db != null)
{
db.Dispose();
db = null;
}
}
}
public IQueryable<User> Users
{
get { return db.Users.Select(u => u); }
}
// IUserStore<TUser>
public Task<string> GetUserIdAsync(User user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));
return Task.FromResult(user.Id.ToString());
}
// IUserStore<TUser>
public Task<string> GetUserNameAsync(User user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));
return Task.FromResult(user.UserName);
}
// IUserStore<TUser>
public Task SetUserNameAsync(User user,
string userName,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));
if (string.IsNullOrEmpty(userName))
throw new ArgumentException("userName");
user.UserName = userName;
return Task.CompletedTask;
}
// IUserStore<TUser>
public Task<string> GetNormalizedUserNameAsync(User user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));
return Task.FromResult(user.UserName);
}
// IUserStore<TUser>
public Task SetNormalizedUserNameAsync(User user,
string normalizedName,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));
if (string.IsNullOrEmpty(normalizedName))
throw new ArgumentException("normalizedName");
return Task.CompletedTask;
}
// IUserStore<TUser>
public async Task<IdentityResult> CreateAsync(User user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));
db.Add(user);
int rows = await db.SaveChangesAsync(cancellationToken);
if (rows > 0)
{
return IdentityResult.Success;
}
return IdentityResult.Failed(
new IdentityError { Description = $"{user.UserName} Register Failed" });
}
// IUserStore<TUser>
public async Task<IdentityResult> UpdateAsync(User user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));
db.Update(user);
int rows = await db.SaveChangesAsync(cancellationToken);
if (rows > 0)
{
return IdentityResult.Success;
}
return IdentityResult.Failed(
new IdentityError { Description = $"{user.UserName} Update Failed" });
}
// IUserStore<TUser>
public async Task<IdentityResult> DeleteAsync(User user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));
db.Remove(user);
int rows = await db.SaveChangesAsync(cancellationToken);
if (rows > 0)
{
return IdentityResult.Success;
}
return IdentityResult.Failed(
new IdentityError { Description = $"{user.UserName} Deletion Failed" });
}
// IUserStore<TUser>
public async Task<User> FindByIdAsync(string userId,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (int.TryParse(userId, out int id))
{
return await db.Users.SingleOrDefaultAsync(u => u.Id == id,
cancellationToken);
}
else
{
return await Task.FromResult<User>(null);
}
}
// IUserStore<TUser>
public async Task<User> FindByNameAsync(string normalizedUserName,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrEmpty(normalizedUserName))
throw new ArgumentException("normalizedUserName");
User user = await db.Users.SingleOrDefaultAsync(
u => u.UserName.Equals(normalizedUserName.ToLower()),
cancellationToken);
return user;
}
// IUserPasswordStore<TUser>
public Task SetPasswordHashAsync(User user,
string passwordHash,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));
if (string.IsNullOrEmpty(passwordHash))
throw new ArgumentException("passwordHash");
user.PasswordHash = passwordHash;
return Task.CompletedTask;
}
// IUserPasswordStore<TUser>
public Task<string> GetPasswordHashAsync(User user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));
return Task.FromResult(user.PasswordHash);
}
// IUserPasswordStore<TUser>
public Task<bool> HasPasswordAsync(User user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));
return Task.FromResult(
!string.IsNullOrWhiteSpace(user.PasswordHash));
}
}
}