1

I'm trying to write a custom ValidationAttribute that verifies no records already exist with the same value.

The problem is that if the user is editing an existing record, then my code will find a record that matches the same value. It will find the one that is being edited if the user has not changed that value.

So I thought I could compare the value to the value in ValidationContext.ObjectInstance to detect when it hasn't changed, something like this:

public class UrlTagValidationAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext context)
    {
        string tag = value as string;
        if (string.IsNullOrWhiteSpace(tag))
            return new ValidationResult("URL Tag is required.");

        // We allow a valid that is equal to the original value
        if (context.ObjectInstance is TrainerModel model && model.Tag == tag)
            return ValidationResult.Success;

        // Cannot be an existing tag
        using (var dbContext = new OnBoard101Entities())
        {
            if (!dbContext.TrainerDetails.Any(td => td.Tag == tag))
                return ValidationResult.Success;
        }

        return new ValidationResult("This URL Tag is not available. Please enter a different one.");
    }
}

But this doesn't work. I find that the value in ValidationContext.ObjectInstance often matches the value entered even when I'm creating a new record.

I'm having a hard time finding good and current documentation on the current usage of ValidationContext. Can someone suggest a way to check if any records exist that match the entered value BUT allowing it when a record is being edited and the value of this field has not changed?

Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466
  • Hey I am just wondering what is in your OnBoard101Entities() definition? Im trying to check for duplicates myself but in the validation there is no access to the EF contexts used by .net core. – Orlando Feb 04 '20 at 19:16
  • @Lord-Link: It's my DbContext class. I was using database-first and an EDMX file. – Jonathan Wood Feb 04 '20 at 19:20

1 Answers1

1

The item which is currently being edited most likely has some kind of property to identify it (look it up in the database). Therefore, you need to get that property so you can exclude that when you are searching the database for duplicate tags. Here is how to do that in your custom validation class. I am making the assumption the identifier is named TrainerId:

public class UrlTagValidationAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext context)
    {
        string tag = value as string;
        if(string.IsNullOrWhiteSpace(tag))
            return new ValidationResult("URL Tag is required.");

        var currentTrainer = validationContext.ObjectInstance 
                                 as TrainerModel;
        if (currentTrainer == null)
        {
            // What do you want to do? You cannot always return an error
            // because another type could be using this custom validation.
            // Or you can return an error. Depends on your requirements and 
            // and usage.
        }
        using(var dbContext = new OnBoard101Entities())
        {
            if(dbContext.TrainerDetails.Any(td => td.Tag == tag && td.TrainerId != 
                                            currentTrainer.TrainerId))
            {
                return new ValidationResult("This URL Tag is not available. Please enter a different one.");
            }
        }

        return ValidationResult.Success;
    }
}
CodingYoshi
  • 25,467
  • 4
  • 62
  • 64
  • This is the approach I ended up taking. Note that you should use `validationContext.ObjectInstance as TrainerModel` and then check the result for `null` to be on the safe side. In the unlikely case that it's `null`, I return an error. – Jonathan Wood Apr 08 '17 at 18:30
  • Yes I figured you would take care of the error handling. – CodingYoshi Apr 08 '17 at 18:32
  • validationContext.ObjectInstance as TrainerModel is not flexible. How would you use the same validator, but on a different model? For example, here you are using a TrainerModel, but what if we want to use a TraineeModel instead and validate against the same rules? – leewilson86 Mar 20 '18 at 00:09
  • @leewilson86 in that case inherit from a common class and cast to that instead. – CodingYoshi Mar 20 '18 at 06:01
  • @CodingYoshi if you inherited fro a common class, then the following would cause a compile error: currentTrainer.TrainerId Maybe the method is doing too many things and implementation details need to be separated from the abstract details? I think the common class idea you mention would work, however, you need to account for currentTrainer.TrainerId – leewilson86 Mar 21 '18 at 14:37
  • @leewilson86 no it will not. You need to make sure all the properties accessed in this validation are in the common class. BTW the OP did not have these requirements; therefore, it is unnecessary to handle all the possible scenarios. If you have a specific question, please ask a new question. – CodingYoshi Mar 21 '18 at 14:57
  • @CodingYoshi. OK, if you are placing this value in the common class it makes sense. However I was considering what if your model has already extended from the common class? Also, to help the OP in getting the best solution, it is relevant to ask questions against peoples responses. This helps to validate everyones thinking, just think you have helped me validate my thinking based upon the feedback you gave RE: the common class. Remember, Stackoverflow is a public platform to help other developers too. This post has helped me, and my requirements extended a little further than the OPers. – leewilson86 Mar 22 '18 at 16:23