CREATING CUSTOM VALIDATION ATTRIBUTE IN .NET
Tags: csharp, dotnet, backend, api
Published: 2023-07-16 22:30:26

In .NET, there are many built-in validation attributes available. However, there might be situations where none of them perfectly fit your requirements. In such cases, you could create a custom validation attribute. This allows you to define your own rules for validating properties or fields in your code. 

If you don't know anything about Validation attribute, I have a post about that on linkedIn. [here](https://www.linkedin.com/posts/adeniyi-adekunle-85a3461b5_model-validation-in-net-activity-7065234623798099968-aaXY?utm_source=share&utm_medium=member_desktop). Essentially, Validation attribute are attributes used to validate and protect a model.

In today's article, we will be creating an attribute that validates file extension. Take for instance, we have a model that contains an IFormFile field and we want to ensure it accept certain file type(we want to ensure only png, jpg, and jpeg files are uploaded). In this case, we could create a custom validation attribute that will handle this.​

## **STEPS IN CREATING A CUSTOM VALIDATION ATTRIBUTE**

**Step 1:** Create a csharp class and call it **CustomFileExtensionValidation** and inherit from ValidationAttribute abstract class.

_A ValidationAttribute is actually a base class for validation attributes like RequiredAttribute, CompareAttribute, MaxLengthAttribute, etc. To create our own validation attribute, we have to inherit from ValidationAttribute class also._

```
using System;
using System.ComponentModel.DataAnnotations;

namespace BookApp.CustomValidations
{
    public class CustomFileExtensionValidation 
    :ValidationAttribute
  {
        public CustomFileExtensionValidation()
        {
        
        }
   }
}
```

**Step 2:** Overriding **IsValid** Method. This is a method in ValidationAttribute class that when override, enable us write our custom validation. We will be overriding this method in our class and we will write all our code in this method​. The method accept an object parameter. From this parameter we could get the value passed to the field we are validating, In our case, we will be getting the uploaded file. We will also define a public field called Extensions that will enable us set our extension(s).

```
using System;
using System.ComponentModel.DataAnnotations;

namespace BookApp.CustomValidations
{
    public class CustomFileExtensionValidation 
    :ValidationAttribute
 {
     public string Extensions { get; set; }
     public CustomFileExtensionValidation()...
         
     public override bool IsValid(object value)
     {
        if (value == null)
           return true;

        return false;
     }
  }
}
```

**Step 3:** Creation of private methods that will handles the validation. In order not to have a lot of code in our IsValid method, we will create two private methods to make our code cleaner. 
The first private method handles validating a single file;


```
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using System.Linq;
using System.Collections.Generic;


namespace BookApp.CustomValidations
{
    public class CustomFileExtensionValidation 
    :ValidationAttribute
  {
    //you can allow multiple extensions by separating each by a coma sign
    public string Extensions { get; set; }
          
    public CustomFileExtensionValidation()...

    public override bool IsValid(object value)...
     
    private bool ValidateFile(IFormFile file)
    {
       if (file == null ||string.IsNullOrEmpty(file.FileName))
       {
        // File is null or empty, return false indicating validation failure
          return false;
       }

       if (!string.IsNullOrEmpty(Extensions))
       {
         //check if the file type uploaded matches any of the extensions defined
         var allowedExtensions = Extensions.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                                                 .Select(ext => ext.Trim())
                                                 .ToList();

        var fileName = file.FileName;

        var isValid = allowedExtensions.Any(ext => fileName.EndsWith(ext, StringComparison.OrdinalIgnoreCase));

        return isValid;
      }
        // File is valid
        return true;
      }
  }
}
```

while the second private method handles validating multiple files;

```
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using System.Linq;
using System.Collections.Generic;


namespace BookApp.CustomValidations
{
    public class CustomFileExtensionValidation 
    :ValidationAttribute
    {
      //you can allow multiple extensions by separating each by a coma sign
      public string Extensions { get; set; }
          
      public CustomFileExtensionValidation()...
        
        
      public override bool IsValid(object value)...
     
      private bool ValidateFile(IFormFile file)...
     
      private bool ValidateFiles(List<IFormFile> files)
      {
         if (files == null || files.Count == 0)
         {
           // Value is null or empty, return false indicating validation failure
            return false;
         }

         if (!string.IsNullOrEmpty(Extensions))
         {
            var allowedExtensions = Extensions.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                                                 .Select(ext => ext.Trim().ToLower())
                                                 .ToList();

          bool isValid = files.All(file =>
          {
            if (file == null || 
            string.IsNullOrEmpty(file.FileName))
            {
              // File is null or empty, return false indicating validation failure
                return false;
            }
          var fileName = file.FileName.ToLower();
                    
        return allowedExtensions.Any(ext => fileName.EndsWith(ext));
          });

          return isValid;
       }
       // All files are valid
       return true;
    }
  }
}
```

**Step 4:** Next, we will include our private methods in our IsValid method.

```
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using System.Linq;
using System.Collections.Generic;


namespace BookApp.CustomValidations
{
    public class CustomFileExtensionValidation 
    :ValidationAttribute
    {
    	public string Extensions { get; set; }
          
        public CustomFileExtensionValidation()...
        
  	public override bool IsValid(object value)
        {
            if (value == null)
                return true;

            if (value is IFormFile file)
                return ValidateFile(file);

            if (value is List<IFormFile> files)
                return ValidateFiles(files);

            return false;
        }
     
        private bool ValidateFile(IFormFile file)...
     
        private bool ValidateFiles(List<IFormFile> files)...
   }
}
```

**Step 5:** Afterward, we will specify the application elements on which to apply the attribute. Here, We will be specifying what type of element we will be validating which could be a field, property, or even method(In this case, we are validating a field). We could also specify if multiple instances of the attribute can be used for an element. In this case, we don't want multiple instances of our attribute on the field we are validating.

```
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using System.Linq;
using System.Collections.Generic;


namespace BookApp.CustomValidations
{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class CustomFileExtensionValidation : ValidationAttribute...
}
```
Learn more about attribute usage from Microsoft official documentation [here](https://learn.microsoft.com/en-us/dotnet/api/system.attributeusageattribute?view=net-7.0).

**Step 6:** We will be taking advantage of the ErrorMessage field in our ValidationAttribute class so we can return a nice error message to our user. In the code below I am setting the field so if no error message is passed into the attribute, one will be sent to the user.

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using System.Linq;

namespace BookApp.CustomValidations
{
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class CustomFileExtensionValidation : ValidationAttribute
	{
        public string Extensions { get; set; }

        public CustomFileExtensionValidation()
        {
            //set a default error message
            ErrorMessage = "Invalid file extension(s)";
        }

        public override bool IsValid(object value)...

        private bool ValidateFile(IFormFile file)...

        private bool ValidateFiles(List<IFormFile> files)...
    }
}

```

**Step 7:** Finally, we will add our attribute to the field we want to validate. Here we want to validate the file type of a file and we only want to accept files with png, jpg and jpeg extension. We are also passing in an error message here.

```
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using BookApp.CustomValidations;
using System.Collections.Generic;

namespace BookApp.Dtos
{
   public class SignUpDTO
   {
      [Required(ErrorMessage = "Email address is required.")]
      public string EmailAddress { get; set; }
      [Range(18,50, ErrorMessage ="The minimum age to be considered for this job is {1} and the maximum is {2}")]
      public int Age { get; set; }

      //Our custom validation attribute
      [CustomFileExtensionValidation(Extensions = "png,jpg,jpeg", ErrorMessage = "Only files with the extensions png, jpg, and jpeg are accepted.")]
      public IFormFile ProfilePicture { get; set; }
    }
}


```

In summary, as much as there are lots of validation attribute, there are certain scenario that requires you to write a custom validation attribute.