Sometimes, we need to check if the input is valid.

Usually, we may do this:

            // Bad example:
            // Requires a lot of duplicate code. Bad architecture.

            if (string.IsNullOrWhiteSpace(InputArgument.TargetMachine))
            {
                throw new ArgumentException($"Invalid input argument! Target Machine: '{InputArgument.TargetMachine}'!");
            }
            if (string.IsNullOrWhiteSpace(InputArgument.PatchId))
            {
                throw new ArgumentException($"Invalid input argument! Patch ID: '{InputArgument.PatchId}'!");
            }
            if (string.IsNullOrWhiteSpace(InputArgument.Password))
            {
                throw new ArgumentException($"Invalid input argument! Password: '{InputArgument.Password}'!");
            }
            if (string.IsNullOrWhiteSpace(InputArgument.UserName))
            {
                throw new ArgumentException($"Invalid input argument! UserName: '{InputArgument.UserName}'!");
            }

But when there is a lot of properties which need to validate, we may need to write a lot of if else.

And, the requirement of the property value, should be describe inside the class itself. The code of defining the rules shouldn't be put to logic code.

How can we validate a C# object in the best practice?

You may think about ASP.NET Core Model validation?

Yes, since ASP.NET Core support model validation like:

Sample code from real ASP.NET Core project:

        [Route("Apps/Create")]
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> CreateApp(CreateAppViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }
            var newApp = new App(model.AppName, model.AppDescription, model.AppCategory, model.AppPlatform, model.IconPath);
            await _dbContext.Apps.AddAsync(newApp);
            await _dbContext.SaveChangesAsync();
            return RedirectToAction(nameof(ViewApp), new { id = newApp.AppId });
        }

As you can see, you can directly call ModelState.IsValid to make sure the model is valid. Also you can build your custom attribute or middleware to ensure all input model is valid.

But how can we use the same approach outside ASP.NET Core?

Use validation in pure C#

First, copy the following function to your project:

        public static (bool isValid, string[] errors) Validate(object input)
        {
            var context = new ValidationContext(input);
            var results = new List<ValidationResult>();

            if (Validator.TryValidateObject(input, context, results, true))
            {
                return (true, Array.Empty<string>());
            }
            else
            {
                return (false, results.Select(t => t.ErrorMessage).ToArray());
            }
        }

When you need to validate, you need to define the rule in the class definition:

    using System.ComponentModel.DataAnnotations;

    public class Widget
    {
        [Required(ErrorMessage = "The {0} is required!")]
        public int? Id { get; set; }

        [Required]
        [MinLength(10, ErrorMessage = "The {0} requires min length: {1}")]
        public string Name { get; set; }

        [Range(1, 100)]
        public decimal Price { get; set; }
    }

And use the following code to do validate:

        public static void Main(string[] args)
        {
            var widget = new Widget
            {
                Price = 1557,
                Name = "test"
            };

            var (isValid, errors) = Validate(widget);

            Console.WriteLine($"Is valid: {isValid}");

            foreach (var error in errors)
            {
                Console.WriteLine(error);
            }
        }

Run result:

What if own customized validation rule?

In some cases, the system provided validation rule might not satisfy the need. And we may need to create our own attribute.

For example, we want to validate the property to not contain space nor wrap.

We can build our own validation attribute like this:

    public class NoSpace : ValidationAttribute
    {
        public override bool IsValid(object value)
        {
            if (value is string val)
            {
                return !val.Contains(" ") && !val.Contains("\r") && !val.Contains("\n");
            }
            return false;
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (IsValid(value))
            {
                return ValidationResult.Success;
            }
            else
            {
                return new ValidationResult($"The {validationContext.DisplayName} can not contain space!");
            }
        }
    }

And use it like:

    public class Widget
    {
        [NoSpace]
        public string Password { get; set; }
    }

            var widget = new Widget
            {
                Password = "Invalid password"
            };

You can get the correct error message:

Full demo code that you can run:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace LearnModelValidate
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var widget = new Widget
            {
                Price = 1557,
                Name = "test",
                Password = "Invalid password"
            };

            var (isValid, errors) = Validate(widget);

            Console.WriteLine($"Is valid: {isValid}");

            foreach (var error in errors)
            {
                Console.WriteLine(error);
            }
        }

        public static (bool isValid, ValidationResult[] errors) Validate(object input, ICollection<ValidationResult> results = null)
        {
            if (results == null)
            {
                results = new List<ValidationResult>();
            }
            var context = new ValidationContext(input);

            // Validate the current object
            bool isValid = Validator.TryValidateObject(input, context, results, true);

            // Use reflection to get properties
            var properties = input.GetType().GetProperties()
                .Where(prop => prop.CanRead && prop.PropertyType.IsClass);

            // Recursively validate nested objects
            foreach (var prop in properties)
            {
                var value = prop.GetValue(input);
                if (value != null)
                {
                    if (value is IEnumerable<object> list)
                    {
                        foreach (var item in list)
                        {
                            var (nestedIsValid, nestedErrors) = Validate(item, results);
                            isValid &= nestedIsValid;
                        }
                    }
                    else
                    {
                        var (nestedIsValid, nestedErrors) = Validate(value, results);
                        isValid &= nestedIsValid;
                    }
                }
            }

            return (isValid, results.ToArray());
        }
    }

    public class Widget
    {
        [Required(ErrorMessage = "The {0} is required!")]
        public int? Id { get; set; }

        [Required]
        [MinLength(10, ErrorMessage = "The {0} requires min length: {1}")]
        public string Name { get; set; }

        [Range(1, 100)]
        public decimal Price { get; set; }

        [NoSpace]
        public string Password { get; set; }
    }

    public class NoSpace : ValidationAttribute
    {
        public override bool IsValid(object value)
        {
            if (value is string val)
            {
                return !val.Contains(" ") && !val.Contains("\r") && !val.Contains("\n");
            }
            return false;
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (IsValid(value))
            {
                return ValidationResult.Success;
            }
            else
            {
                return new ValidationResult($"The {validationContext.DisplayName} can not contain space!");
            }
        }
    }
}