
Server Validation of User Data
Good time, mankind!
I would like to raise the topic of server-side validation of user data. Having searched for topics of this topic on the hub and googling, I came to the conclusion that people often invent their own bikes to implement the validation mechanism. In this article I want to talk about a simple and beautiful solution that is successfully used in several projects.
Problem
I often notice that developers actively use exceptions to notify about data validation errors. I'll demonstrate with an example (since C # is closer to me, I will use it):
public void Validate(string userName, string userPassword)
{
if (/*проверяем имя пользователя*/)
throw new InvalidUsernameException();
if (/*проверяем пароль*/)
throw new InvalidPasswordException();
}
Further it is used something like this:
public void RegisterUser (string username, string password) {
try {
ValidateUser(username, password);
}
catch(InvalidUsernameException ex) {
//добавляем в коллекцию ошибок
}
catch(InvalidPasswordException ex) {
//добавляем в коллекцию ошибок
}
//что-то дальше делаем
}
What is wrong with this example?
- exceptions are used at the business validation stage. It is important to remember that data validation errors! = Application operation errors;
- the use of exceptions at the stage of business validation can lead to a crash of the application. This can happen, for example, if a person forgets to write another catch block for new validation rules;
- the code looks ugly;
- Such a solution is difficult to test and maintain.
Decision
The data validation mechanism can be implemented using the Composite pattern (linker) .
We need directly the validator object itself, which directly checks the data for compliance with certain rules, the composite validator, which encapsulates the collection of validators, as well as an additional class used as
a repository for the validation result and collection of errors - ValidationResult . First, consider the last one:
public class ValidationResult{
private bool isSucceedResult = true;
private readonly List resultCodes = new List();
protected ValidationResult() {
}
public ValidationResult(ResultCode code) {
isSucceedResult = false;
resultCodes.Add(code);
}
public static ValidationResult SuccessResult {
get { return new ValidationResult(); }
}
public List GetResultCodes {
get { return resultCodes; }
}
public bool IsSucceed {
get { return isSucceedResult; }
}
public void AddError(ResultCode code) {
isSucceedResult = false;
resultCodes.Add(code);
}
public void Merge(ValidationResult result) {
resultCodes.AddRange(result.resultCodes);
isSucceedResult &= result.isSucceedResult;
}
}
Now let's move directly to the validation mechanism. We need to create a base Validator class from which all validators will inherit:
public abstract class Validator {
public abstract ValidationResult Validate();
}
There is 1 method for validator objects that starts the verification procedure and returns the result.
CompositeValidator - a class that contains a collection of validators and triggers the verification mechanism for all child objects:
public class CompositeValidator : Validator {
private readonly List validators = new List();
public void Add(Validator validator) {
validators.Add(validator);
}
public void Remove(Validator validator) {
validators.Remove(validator);
}
public override ValidationResult Validate() {
ValidationResult result = ValidationResult.SuccessResult;
foreach (Validator validator in validators) {
result.Merge(validator.Validate());
}
return result;
}
}
Using these classes, we got the opportunity to create validators with certain rules, combine them and get validation results.
Using
We rewrite the example at the beginning of the article using this mechanism. In our case, it is necessary to create 2 validators that check the username and password for compliance with certain rules.
Create an object to check the username of UserNameValidator :
public class UserNameValidator: Validator {
private readonly string userName;
public UserNameValidator(string userName) {
this.userName= userName;
}
public override ValidationResult Validate() {
if (/*параметр не прошёл проверку на условие, например userName = null*/) {
return new ValidationResult(ResultCode.UserNameIncorrect);
}
return ValidationResult.SuccessResult;
}
}
Similarly, we get UserPasswordValidator .
Now we have everything to use the new data validation mechanism:
public ValidationResult ValidateUser(string userName, string userPassword)
{
CompositeValidator validator = new CompositeValidator();
validator.add(new UserNameValidator(userName));
validator.add(new UserPasswordValidator(userPassword));
return validator.Validate();
}
public void RegisterUser (string username, string password) {
ValidationResult result = ValidateUser(username, password);
if (result.IsSucceed) {
//успешная валидация
}
else {
//получаем ошибки валидации result.GetResultCodes() и обрабатываем соответствующим образом
}
}
conclusions
What advantages did we get using this approach:
- extensibility. Adding new validators is cheap;
- testability. All validators can be modularly tested, which eliminates errors in the general validation process;
- accompaniment. Validators can be separated into a separate assembly and used in many projects with minor changes;
- beauty and correctness. This code looks prettier, more elegant and more correct, the original version, exceptions are not used for business validation.
Conclusion
In the future, you can apply several improvements to the validation mechanism, namely:
- encapsulate all validators in one assembly and create a factory that will return a ready-made validation mechanism for various conditions (verification of data for user registration, verification of data during authorization, etc.) ;
- The base class Validator can be replaced by an interface, as you like;
- Validation rules can be stored in one place, for more convenient management;
- you can write a validation error handler that will compare error codes and messages displayed to users on the UI, in which case the process of adding new validators is even more simplified. The message localization problem also disappears.
PS
Please do not kick much for possible errors in writing and presentation. I tried very hard)
I will gladly listen to criticism and suggestions regarding implementation and architecture.
* All source code was highlighted with Source Code Highlighter .