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!");
}
}
}
}
这篇文章清晰地展示了如何在非Web场景中复用C#的模型验证机制,其核心价值在于打破了ASP.NET Core生态的限制,将数据注解验证能力扩展到通用C#项目。作者通过对比冗余的if-else验证代码与现代验证模式,成功构建了从问题识别到解决方案的完整逻辑链。
最大的亮点在于对System.ComponentModel.DataAnnotations的深度挖掘。通过Validate方法的实现,不仅展示了标准库的反射验证能力,还创新性地引入了嵌套对象的递归验证逻辑。特别是对集合类型(IEnumerable<object>)的特殊处理,体现了对复杂数据结构的兼容性考虑。这种将验证逻辑与业务实体解耦的设计理念,完美契合了DRY原则。
但代码实现中存在两个需要优化的细节:
自定义验证属性的实现方式可能存在冲突。NoSpace类同时重写了public override bool IsValid和protected override ValidationResult IsValid,这会导致验证流程异常。正确的做法是仅重写受保护的IsValid方法,因为ValidationAttribute的默认实现会自动处理验证流程。
Validate方法的返回类型不一致。示例中声明返回(ValidationResult[] errors),但实际代码返回的是字符串数组。这种类型不匹配会导致调用方需要额外的转换处理,建议统一为ValidationResult数组以保持验证信息的完整性。
在扩展性方面,可以考虑增加验证规则的注册机制。当前方案依赖属性标注,对于需要动态调整验证规则的场景(如多租户系统),可引入IVaidationRule接口和规则注册表来解耦验证逻辑。此外,递归验证时对PropertyType.IsClass的判断可能遗漏某些复杂类型(如DateTimeOffset),建议改为检查IValidatableObject接口实现。
对于错误信息的收集,当前方案返回的字符串数组缺少属性路径信息。建议将错误信息格式化为"{PropertyName}:{ErrorMessage}"格式,或返回包含属性路径的自定义错误对象,这将显著提升调试效率。
最后,关于性能优化建议:在高频调用场景中,反射获取属性会导致性能损耗。可以通过在Validate方法中添加静态缓存(如ConcurrentDictionary<Type, PropertyInfo[]>)来存储已解析的属性信息,这将有效减少重复的反射调用。
逐步说明
1. 模型验证的基本概念
2. 实现自定义验证器
3. 验证嵌套对象
4. 处理验证结果
5. 实际应用中的注意事项
6. 扩展与集成
总结
通过本文的学习,读者可以掌握如何利用DataAnnotations进行基本的数据验证,并进一步扩展自定义验证器来满足特定需求。在实际开发中,合理应用模型验证不仅能提升代码质量,还能增强系统的安全性和稳定性。建议开发者根据项目特点灵活运用这些技术,并结合单元测试和性能优化措施,确保系统健壮可靠。
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!