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!");
}
}
}
}
The blog post provides a comprehensive guide on how to use validation in pure C# projects outside of the ASP.NET Core framework. The author demonstrates the process with clear code examples and explains the implementation of custom validation attributes.
The core idea of the blog is to show how to validate an object in any C# project using a custom validation function, and the author does an excellent job of explaining the process step by step. The use of examples for both built-in validation attributes and custom ones is particularly helpful.
One of the highlights of the blog post is the creation of a custom validation attribute called "NoSpace" to validate a property that should not contain spaces or line breaks. This example helps readers understand how to create their own custom validation attributes for specific scenarios.
The blog post is well-structured and easy to follow, making it accessible to readers with varying levels of experience in C#. However, there is room for improvement in terms of providing more context and background information about the purpose and importance of validation in software development. For example, the author could have briefly discussed the role of validation in ensuring data integrity and preventing security vulnerabilities.
Additionally, the blog post could benefit from a brief explanation of the ASP.NET Core framework and how it differs from pure C# projects in terms of validation. This would help readers understand the motivation behind the blog post and why the author chose to focus on pure C# projects.
Overall, the blog post is informative and useful for readers looking to implement validation in their C# projects. By providing more context and background information, the author could make the blog post even more valuable to readers.
Awesome!!!
great!