Kes
Kes

Reputation: 498

Override ValidateAsync in UserValidator.cs for .NET Core Identity 2.1

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:

  1. While debugging, DuplicateUserName() is not called, but still receiving duplicate username error.
  2. Tested for other username validation by putting special characters. Failed validation with the special characters not allowed error twice!

What am I doing wrong or missing here? Thanks in advance.

Upvotes: 6

Views: 3955

Answers (2)

philw
philw

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

ccassob
ccassob

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

Related Questions