Reputation: 498
I'm customizing the validation for username to allow the same username (non-unique). This is with an additional field "Deleted" as a soft delete to identity user. So the customization involves changing the current validation to check if the username already exist and deleted is false to only trigger DuplicateUserName error.
What I've done is create a CustomUserValidator class, and override the ValidateAsync method in UserValidator.cs as well as the ValidateUserName method. Below is the code:
CustomUserValidator.cs
public class CustomUserValidator<TUser> : UserValidator<TUser>
where TUser : ApplicationUser
{
public override async Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user)
{
if (manager == null)
{
throw new ArgumentNullException(nameof(manager));
}
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var errors = new List<IdentityError>();
await ValidateUserName(manager, user, errors);
if (manager.Options.User.RequireUniqueEmail)
{
await ValidateEmail(manager, user, errors);
}
return errors.Count > 0 ? IdentityResult.Failed(errors.ToArray()) : IdentityResult.Success;
}
private async Task ValidateUserName(UserManager<TUser> manager, TUser user, ICollection<IdentityError> errors)
{
var userName = await manager.GetUserNameAsync(user);
if (string.IsNullOrWhiteSpace(userName))
{
errors.Add(Describer.InvalidUserName(userName));
}
else if (!string.IsNullOrEmpty(manager.Options.User.AllowedUserNameCharacters) &&
userName.Any(c => !manager.Options.User.AllowedUserNameCharacters.Contains(c)))
{
errors.Add(Describer.InvalidUserName(userName));
}
else
{
//var owner = await manager.FindByNameAsync(userName);
var owner = manager.Users.Where(x => !x.Deleted &&
x.UserName.ToUpper() == userName.ToUpper())
.FirstOrDefault();
if (owner != null &&
!string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user)))
{
errors.Add(Describer.DuplicateUserName(userName));
}
}
}
}
And in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IUserValidator<ApplicationUser>, CustomUserValidator<ApplicationUser>>();
}
The code in ValidateAsync method in CustomUserValidator works fine, but it seems the original ValidateAsync is running as well. Why I say so is because:
What am I doing wrong or missing here? Thanks in advance.
Upvotes: 6
Views: 3955
Reputation: 687
The syntax for this in Core 3.1 is slightly different:
services.AddTransient<IUserValidator<ApplicationUser>, CustomUserValidator>();
Where CustomUserValidator interits from the standard user validator which is in the source here. That's necessary if you're removing validation. Note that some of the validation (eg User Name uniqueness) is backed up by database constraints so you'll maybe need to tweak those also.
If you want to simply add more validation, then you can just add your own validator as an additional service to Identity in startup.cs.
Upvotes: 0
Reputation: 353
First let me explain the problem
The reason is why the Identity library injects the validate user class that uses the default library, which is UserValidator.
The solution is only to inject a CustomUserValidator. What happens is that if it is injected into the normal implementation, what it does is that it adds 2 UserValidator, the first is the default by the identity library and the second would be the one that you implemented the CustomUserIdentity.
Then to inject only the CustomUserIdentity you must create a new CustomUserManager to be able to inject the new ICustomUserValidator so that it does not take the one that is by default IUserValidator.
This is my solution:
This is the interface ICustomUserValidator
public interface ICustomUserValidator<TUser> : IUserValidator<TUser> where TUser : ApplicationUser
{
}
And the implementation of the class
public class CustomUserValidator<TUser> : UserValidator<TUser>, ICustomUserValidator<TUser>
where TUser : ApplicationUser
{
public async Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user)
{
//Some Code
}
private async Task ValidateUserName(UserManager<TUser> manager, TUser user, ICollection<IdentityError> errors)
{
//Some Code
}
}
And this one for the CustomUserManager
public class CustomUserManager<TUser> : UserManager<TUser>
where TUser : ApplicationUser
{
public CustomUserManager(IUserStore<TUser> store, IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<TUser> passwordHasher, IEnumerable<ICustomUserValidator<TUser>> userValidators,
IEnumerable<IPasswordValidator<TUser>> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider tokenProviders,
ILogger<UserManager<TUser>> logger)
: base(
store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors,
tokenProviders, logger)
{
}
}
Notice that instead of IUserValidator, I put ICustomUserValidator
In the Startup class you have to inject the new class:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddUserManager<CustomUserManager<ApplicationUser>>()
And finally inject this class
services.AddTransient<ICustomUserValidator<ApplicationUser>, CustomUserValidator<ApplicationUser>>();
I hope this implementation helps you.
Upvotes: 10