What are the best practices in field validation?

Asked

Viewed 8,119 times

18

I created an example to illustrate my doubt regarding field validation, the example is composed of two classes Pessoa and ModelPessoa. The class Pessoa has the following attributes: Nome, Idade, Endereco, Salario and Cargo, and my class ModelPessoa has the method int ValidarCampos(Pessoa funcionario) this method is responsible for checking the fields according to the business rule.

The rules set as an example to illustrate the doubt are as follows::

  • Name is required;
  • Address is required;
  • Position is mandatory;
  • Salary must contain value above zero;
  • Employee has to be adult;

This is the scope of the method ValidarCampos defined in accordance with the above rules:

public int ValidarCampos(Pessoa funcionario) 
{
    if (string.IsNullOrEmpty(funcionario.Nome)) //Verifica se há conteúdo na variável.
        return 1;
    if (string.IsNullOrEmpty(funcionario.Endereco)) //Verifica se há conteúdo na variável.
        return 2;
    if (string.IsNullOrEmpty(funcionario.Cargo)) //Verifica se há conteúdo na variável.
        return 3;
    if (funcionario.Salario <= 0) //Verifica se o valor do salário é valido.
        return 4;
    if (funcionario.Idade < 18) //Verifica se o funcionário é maior de idade.
        return 5;

    return 0; //Todos os campos atende os requisitos das regras.
}

Below is the implementation of the method ValidarCampos and of the two classes:

Pessoa funcionario = new Pessoa();
ModelPessoa mp = new ModelPessoa();

Console.Write("Nome: ");
funcionario.Nome = Console.ReadLine().ToString();

Console.Write("Endereço: ");
funcionario.Endereco = Console.ReadLine().ToString();

Console.Write("Cargo: ");
funcionario.Cargo = Console.ReadLine().ToString();

Console.Write("Salario: ");
funcionario.Salario = Convert.ToDouble(Console.ReadLine());

Console.Write("Idade: ");
funcionario.Idade = Convert.ToInt32(Console.ReadLine());

switch (mp.ValidarCampos(funcionario)) 
{
    case 1:
        Console.WriteLine("Nome é obrigatório.");
        break;
    case 2:
        Console.WriteLine("Endereço é obrigatório");
        break;
    case 3:
        Console.WriteLine("Cargo é obrigatório.");
        break;
    case 4:
        Console.WriteLine("Salario deve conter valor acima de zero.");
        break;
    case 5:
        Console.WriteLine("Funcionario tem que ser adulto.");
        break;
    default: 
        Console.WriteLine("Cadastrato com sucesso."); 
        break;
}

This is the whole code of my example for you to reproduce:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ValidacaoCampo
{
    class Pessoa 
    {
        //Todos os campos (atributos) são obrigatorios.
        public string Nome { get; set; } 
        public int Idade { get; set; }
        public string Endereco { get; set; }
        public double Salario { get; set; }
        public string Cargo { get; set; }

        public Pessoa() 
        {
            Nome = "";
            Idade = 0;
            Endereco = "";
            Salario = 0;
            Cargo = "";
        }
    }

    class ModelPessoa 
    {
        public ModelPessoa() { }

        public int ValidarCampos(Pessoa funcionario) 
        {
            if (string.IsNullOrEmpty(funcionario.Nome))
                return 1;
            if (string.IsNullOrEmpty(funcionario.Endereco))
                return 2;
            if (string.IsNullOrEmpty(funcionario.Cargo))
                return 3;
            if (funcionario.Salario <= 0)
                return 4;
            if (funcionario.Idade < 18)
                return 5;

            return 0;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Pessoa funcionario = new Pessoa();
            ModelPessoa mp = new ModelPessoa();

            Console.Write("Nome: ");
            funcionario.Nome = Console.ReadLine().ToString();

            Console.Write("Endereço: ");
            funcionario.Endereco = Console.ReadLine().ToString();

            Console.Write("Cargo: ");
            funcionario.Cargo = Console.ReadLine().ToString();

            Console.Write("Salario: ");
            funcionario.Salario = Convert.ToDouble(Console.ReadLine());

            Console.Write("Idade: ");
            funcionario.Idade = Convert.ToInt32(Console.ReadLine());

            switch (mp.ValidarCampos(funcionario)) 
            {
                case 1:
                    Console.WriteLine("Nome é obrigatório.");
                    break;
                case 2:
                    Console.WriteLine("Endereço é obrigatório");
                    break;
                case 3:
                    Console.WriteLine("Cargo é obrigatório.");
                    break;
                case 4:
                    Console.WriteLine("Salario deve conter valor acima de zero.");
                    break;
                case 5:
                    Console.WriteLine("Funcionario tem que ser adulto.");
                    break;
                default: 
                    Console.WriteLine("Cadastrato com sucesso."); 
                    break;
            }   
        }
    }
}

My question is about how to validate the fields so that the code does not become repetitive and difficult to read, within good programming practices or if there is any Pattern which can be used for field validation? The above method is even possible, but if I have a table with many fields and most fields require some criterion or validation rule, how could the validation be done without leaving the bad code structured, difficult to read and repetitive?

  • 3

    Two comments that have nothing to do with what you ask: "there is" of the verb "there" ("Checks the content in the variable." ) and "if (employee.Age < 18)" instead of "if (employee.Age <= 18)".

  • Thanks, I didn’t notice that.

  • You are using ASP.NET MVC?

  • I’m using a layered architecture, but it’s very similar to MVC, but the focus of this layered architecture is desktop applications.

  • 1
  • @Laerte the system I’m developing is for desktop.

Show 1 more comment

3 answers

20


Error codes

First I have to say that I do not like very much the solution that returns the error codes. It is not totally wrong and it is better to launch exception, which many programmers would do because they follow cake recipe. I liked the creativity. But such numerical codes are bad to identify what the error really is. In a simple example and learning is ok. But in production code it’s not much.

This enumeration could indicate the type of invalidity that occurred and not in which property it occurred. For this you would need to modify the validation a little.

I repeat, it is acceptable to use it like this, but I would change to at least use an enumeration instead of meaningless numbers.

Or create classes of invalidities that are similar to exceptions but without having the exception.

Until a return string error would be better than returning a numeric code. Although not in all scenarios.

I created a question and answer pair about this, which is one of my posts that I like the most.

Modularization

The form used to validate is not wrong, but it does not follow the precepts of modularization, of object orientation, as some like. Each validation should be in its own method.

There are even those who argue in some cases that individual validations should be in their own classes and not in a single class. This may be an exaggeration or not. Doing this makes it easier to exchange validations. You can use Strategy Pattern, for example. Perhaps other patterns may be needed to compose the creation of the object properly.

But you need to ask yourself if this is necessary, as well as if it is necessary to have the ModelPessoa. There must be reasons to increase the complexity of the application.

It’s not absurd to have it, but if you can’t justify it, you’re overengineering it. The fact that you change one class, force another, is not always a good idea. But there are cases that you can’t get away from without disorganizing the code. It seems to me that Pessoa is already yours model.

What many people do, and it can be right or wrong depending on the context, is to create classes for property with everything it needs, each property is self-sufficient in its own kind. This is usually considered an anti-pattern that I don’t remember the name now. It makes some sense, but it can be exaggerated, especially if you’re doing it without being aware of the real need.

This form can be advantageous for leaving all that pertains to that property close by. But it would have to make a change that would make sense even if the validation is within ModelPessoa or within Pessoa. Validation made through a method by property could encapsulate everything that would need to be validated for that property, and would have one place to change when something in the property changes.

On the other hand, validation could be included within the property, specifically within the set. Of course, there is a semantic difference between doing this and having a separate method. Inside the property it can never be in invalid state, and you may need it eventually. In addition to what you are restricted to report the invalidity by exception. I still think the best solution is to have validation methods. You have more control.

But not all validation can be done only on the property. There are global validations that complicate this scenario of approximating validation to its state.

Attributes

Another way to bring the validation closer to the property without creating individual classes for each property is to use attributes. For this you need a library of them.

Flawed method

This form used in the question is very bad because if the first property is invalid, it will report this problem and no other. It works, but gives a bad experience. It would have to return a list with all the errors, or a numeric code that operates as flags booleans to indicate which errors occurred at each bit, or completely change the way to handle validation.

A yield could be a solution, but it doesn’t solve at all.

Constructor and startup

As a last parallel note, since I know you want to learn right, if you are using C#6, the builder is unnecessary since it allows initializing properties. Even in earlier versions it can be replaced by initializations within the class, but the code would get bigger because it would have to stop using the default property and initialize the private field it encapsulates. To ModelPessoa certainly does not need the builder.

Library

Now coming to the direct subject of the question, you could create a library of validations that often happen. But it would have to work with a parameter that would be the property to be validated. It would not work with the whole object. There’s even a whim, but it wouldn’t do any good.

This can be useful for maintenance including because if there is a different way to do that specific validation, changed in the library, changed to the entire application.

This is called abstraction. Let’s say we started using IsEmpty() to validate. Then one day discovers that the IsNullOrEmpty() was better. Imagine having to search for every application and change it. A search global can help but would need to manually analyze each case, not everything would be validation, somewhere may have done the same kind of validation in a different way. If you’re abstracted/encapsulated in one place it’s easy to solve.

Another example: if you change the age of majority. Or if the check is bug. This information should be in only one place. And there may be different majorities for each case, all this has to be thought out. Almost no one thinks about these things. When you think about it, it’s usually too late, the software is already all disorganized. Programming is very difficult and so there are so many software full of bugs. It is common, even the best programmers, can not see the whole problem. Of course the more experienced know how to create easy escapes without generating complication.

This is to do DRY, which is one of the most important things in programming.

Will it give a very big gain in simple validations like this, as the question asks? No. But DRY, abstraction, does not exist to make the code smaller. It determines that the change is needed at only one point.

The gain can be greater in more complex validations. But these tend to be less reusable.

Frameworks

This library can be made in attributes form. But its use starts to make sense in frameworks. Because validation will not occur alone. Some mechanism will have to ensure that it is done. This can be obtained in a framework existing, or one you will create. Already exists something more or less ready in . Net. I am not saying that it is the definitive solution and that you should adopt it without thinking. Everything has its problems.

The framework can automate validation checking. You can pick up error messages, for example. Although if you change the error code to an error class, this can already be improved, then you would have something like this:

Console.WriteLine(validacao.MessageInvalidation);

This is a beautiful generalization. In this case it would have an error class that one of the error properties would be MessageInvalidation.

It can make a loop to perform all the necessary validations and this is hidden inside it.

But make a framework is not simple task, in general inexperienced people make so many mistakes that can bring more harm than benefit.

And framewoks poorly made can plaster the application.

Each user interface may need one framework different, even when the user is indirect.

Note that this helps the consumption of validations and not their creation, which is the focus of the question.

Even if you use a library, this cannot replace an abstraction. After all you have to have a way to change the validations individually canonically.

Variant validations

One time you start to encounter more complex problems, which would even make it impossible to validate the solution within the property and even attributes. There are cases where the validation of the object creation is different from the object change, and even depends on the type of change. Think about this. Any generic solution has to consider all possible scenarios.

And the validations that are interdependent. Have you thought about this?

In general people have a lot of difficulty in creating generic software. And when they go to make a mistake, they miss not only by forgetting certain aspects, but also by abstracting unnecessary things, generating complexity for nothing. This is reflected in the use of the object-oriented paradigm. It is very difficult to get the point right, so I usually say that almost nobody knows how to program in this paradigm. Even I think about all this mistake enough. Imagine who can’t handle it.

Design Pattern

The closest pattern I can think of that might help a little bit, but that doesn’t mean I’m going to eliminate repetitions, it’s the Specification Pattern. But if you abuse it, it can become a burden. This can give another look at how validation can be done.

Another that can be used, is basically what the attributes do, is the Decorator Pattern. Another is the Visitor Pattern. They and the Strategy I think it’s better than creating this ModelPessoa in some scenarios. But again, it may be Overkill.

Extension methods can be used to implement these standards, or something close to them, in a simpler way.

And other patterns can be used together according to every need.

Completion

I see ways to better organize the code, better prepare it for maintenance, but I see little chance of making it less repetitive. Not least because I didn’t see repetition there. Maybe I missed something. Repetition is more in the validation consumption, not in the validation itself.

A article that can give some ideas if you need a complex solution. I don’t like everything about her, but it helps to understand what can be done.

Practical example

Note that this example was done running, without testing too much - it has several errors, and is not even close to trying to show the best way. It just shows one possible way, without thinking too much about all the consequences and needs. The example can be used to have ideas, but not as a recipe. In fact I did a lot of things that might be unnecessary, I did just to show different scenarios.

In real code I would certainly do a little different. In test code, simple, I wouldn’t do any of this. This code was meant to show a middle ground between a fictional example and a real-world example. When we do real applications, certain logics cannot be applied. Deleting repetition is great, but things are not that repetitive. When we generalize something to be aware of what we are doing, sooner or later we will have problems.

The code is a simplification. I can imagine a huge list of improvements, and I’ve found some bugs.

using System;
using static System.Console;
using System.Collections.Generic;
public class Pessoa {
    public string Nome { get; set; } = "";
    public string Endereco { get; set; } = "";
    public int Idade { get; set; } = 0;
    public string Cargo { get; set; } = "";
    public decimal Salario { get; set; } = 0M;
}
public class ModelPessoa : Model<Pessoa> { //adotei a ideia que o Model seria para validar mesmo.
    override public bool Validate() {
        Required("Nome", Value.Nome);
        ValidaEndereco();
        Required("Cargo", Value.Cargo);
        Custom("Salário", "SalarioValido", "O Salário não atende os requisitos", ValidaSalario);
        Range("Idade", Value.Idade, Util.GetMaioridade(), 120);
        return (IsValid = Errors.Count == 0);
    }
    public int AsIdade(string texto) { //isto não é tão necessário, mas quis mostrar esta possibilidade
        var idade = 0;
        if (int.TryParse(texto, out idade)) {
            return idade;
        } else {
            Errors["IdadeInconsistente"] = new Invalidation("Idade", "A Idade foi digitada de forma inconsistente");
            return 0;
        }
    }
    public decimal AsSalario(string texto) { //isto funciona como uma abstração/encapsulamento da funcionalidade
        var salario = 0M;
        if (decimal.TryParse(texto, out salario)) {
            return salario;
        } else {
            Errors["SalarioInconsistente"] = new Invalidation("Salario", "O Salário foi digitado de forma inconsistente");
            return 0;
        }
    }
    private void ValidaEndereco() { //tem a vantagem de que pode mudar a regra facilmente tendo um método isolando a funcionalidade
        if (!Util.IsStandardAddress(Value.Endereco)) {
            AddError("EnderecoPadrao", "Endereço", "EnderecoPadrao", "O Endereço está em formato inválido.");
        }
    }
    private bool ValidaSalario(object[] values) { //feito para operar com o Custom, pode mudar as regras fácil aqui de forma canônica
        return ((!Errors.ContainsKey("IdadeInconsistente") && Value.Idade > 21 && Value.Salario > 1000M)) ||
            (Value.Cargo != "Gerente" && Value.Salario > 900M) ||
            (Value.Cargo == "Gerente" && Value.Salario > 1200M);
    }
}
public static class Util { //esta classe foi só para agrupar, em código real estes métodos estariam em outras classes
    public static string Read(string label) {
        Write(label);
        return ReadLine();
    }
    public static int GetMaioridade() {
        return 18; //aqui poderia estar pegando de um banco de dados ou arquivo de configuração
    }
    public static bool IsStandardAddress(string address) {
        return address.Length > 2 && (address.Substring(0, 3) == "Rua" || address.Substring(0, 3) == "Av.");
    }
}
public class Program {
    public static void Main(string[] args) {
        while (true) {
            var mp = new ModelPessoa(); //o modelo é usado para trabalhar com dados temporários
            mp.Messages["Idade"] = "A pessoa precisa ser maior de idade"; //personalizando uma mensagem
            mp.Value.Nome = Util.Read("Nome: "); //lê o dado e guarda no modelo
            mp.Value.Endereco = Util.Read("Endereço: ");
            mp.Value.Idade = mp.AsIdade(Util.Read("Idade: ")); //tentando converter
            mp.Value.Cargo = Util.Read("Cargo: ");
            mp.Value.Salario = mp.AsSalario(Util.Read("Salário: "));
            if (mp.Validate()) {
                Pessoa funcionario = mp.Value; //se está válido, então pode jogar em um objeto definitivo, gravar em DB, etc
                WriteLine("Cadastro efetuado!");
                break;
            } else {
                WriteLine("Erros ocorreram!");
                foreach(var erro in mp.Errors) { //tem várias formas para mostrar os erros
                    WriteLine(erro.Value);
                }
            }
        }
    }
}
public class Invalidation { //uma classe simplificada para guardar dados completos sobre uma invalidade, parecido com exceção, sem ser
    public string Data { get; private set; }
    private string message;
    public string Message {
        get {
            return message ?? "Dado inválido";
        }
    }
    public Invalidation(string data, string message = null) {
        Data = data;
        this.message = message;
    }
    public override string ToString() {
        return Message;
    }
}
public abstract class Model<T> where T : new() { //mecanismo básico de validação separado do modelo em si. Tem maneiras mais simples de fazer o controle de erros
    public T Value { get; private set; } = new T();
    public bool IsValid { get; protected set; } = false;
    public Dictionary<string, Invalidation> Errors { get; protected set; } = new Dictionary<string, Invalidation>();
    public Dictionary<string, string> Messages { get; set; } = new Dictionary<string, string>();
    public abstract bool Validate();
    protected void Required(string field, string value) { //alguns exemplos de códigos genéricos de validação
        if (string.IsNullOrEmpty(value)) {
            AddError(field + "Required", field, "Required", "O campo {0} é obrigatório.");
        }
    }
    protected void Range(string field, int value, int min, int max) {
        if (value < min || value > max) {
            AddError(field + "Range", field, "Range", "O campo {0} deve estar na faixa de {1} até {2}.", min, max);
        }
    }
    protected void Custom(string field, string messageKey, string messageAlt, Func<object[], bool> condition, params object[] values) {
        if (!condition(values)) {
            if (values != null && values.Length > 0) {
                AddError(field + messageKey, field, messageKey, messageAlt, values?[0], values?[1], values?[2]); //está porco, mas não vou gastar tempo
            } else {
                AddError(field + messageKey, field, messageKey, messageAlt);
            }
        }
    }
    protected void AddError(string errorKey, string field, string messageKey, string defaultMessage, object value1 = null, object value2 = null, object value3 = null) {
        var message = "";
        Errors[errorKey] = new Invalidation(field, string.Format(Messages.TryGetValue(messageKey, out message) ? message : defaultMessage, field, value1, value2, value3));
    }
}

Behold working in the ideone. And in the .NET Fiddle. Also put on the Github for future reference.

If I have time and the AP thinks it’s good, I can see if I can make a version like framework using attributes and automation and generalization of some tasks. I do not promise.

  • Thanks for the reply bigown, I will study further in.

  • 2

    Then you can ask more specific questions about this. The subject yields.

  • I will implement your example to see how it works and learn more about it, but as you said the rules and the logic changes, sometimes it depends on the password, but your example is already a big step, I found very interesting as your example, I still have to finish itlo, if you want you can make a version as framework I support the idea, will help a lot of people.

  • In class Model what this expression means: Model<T> where T : new() ?

  • 1

    It’s kind of complicated to answer here in the comment, I think we could fit a question.

  • I will draw up a :)

Show 1 more comment

6

In fact has a lot of code repetition there and you have reason to be worried.

C# provides great object orientation features to favor reuse and prevent code repetition.

You can generalize the code, simplifying it.

Good practice in field validation starts with good practice common to any C#code, such as promoting reuse, preferring declarative code over imperative, reducing cyclomatic complexity...

Generalizing to remove Ifs and Cases

How about instead of doing this for each field:

if (string.IsNullOrEmpty(funcionario.Nome))
{
    return 1 // código de erro;
}

You could just do that:

ValidaCampoObrigatorio("Nome", funcionario.Nome, mensagem, errosValidacao);

How about instead of doing it for every field:

case 1:
    Console.WriteLine("Nome é obrigatório.");
    break;

You could just do that:

Console.WriteLine(erro);

Would it get better?

In the code below, I generalized the validation by type of validation and instead do a if to validate each field I made only one if for type of validation.

This solution also dispenses with error codes by replacing them with a list of validation errors.

As a bonus, the switch case was removed.

class Pessoa
{
    public string Nome { get; set; }
    public int Idade { get; set; }
    public string Endereco { get; set; }
    public double Salario { get; set; }
    public string Cargo { get; set; }
}

class ModelPessoa
{
    public List<string> ValidarCampos(Pessoa funcionario)
    {
        var errosValidacao = new List<String>();

        var msgCampoObrigatorio = "{0} é obrigatório.";

        CampoObrigatorio("Nome", funcionario.Nome, msgCampoObrigatorio, errosValidacao);
        CampoObrigatorio("Endereço", funcionario.Endereco, msgCampoObrigatorio, errosValidacao);
        CampoObrigatorio("Cargo", funcionario.Cargo, msgCampoObrigatorio, errosValidacao);
        ValorMinimo("Salário", 0.01, funcionario.Salario, "Salário deve conter valor acima de zero." , errosValidacao);
        ValorMinimo("Idade", 18, funcionario.Idade, "Funcionário tem que ser adulto.", errosValidacao);

        return errosValidacao;
    }
    private void ValorMinimo(string nomeCampo, double valorMinimo, double valor, string mensagemPattern, List<string> errosValidacao)
    {
        if (valor < valorMinimo)
            errosValidacao.Add(string.Format(mensagemPattern, nomeCampo, valorMinimo));
    }
    private void CampoObrigatorio(string nomeCampo, string valor, string mensagemPattern, List<string> errosValidacao)
    {
        if (String.IsNullOrEmpty(valor))
            errosValidacao.Add(string.Format(mensagemPattern, nomeCampo));
    }
}

class Program
{
    static void Main(string[] args)
    {
        Pessoa funcionario = new Pessoa();

        Console.Write("Nome: ");
        funcionario.Nome = Console.ReadLine().ToString();

        Console.Write("Endereço: ");
        funcionario.Endereco = Console.ReadLine().ToString();

        Console.Write("Cargo: ");
        funcionario.Cargo = Console.ReadLine().ToString();

        Console.Write("Salario: ");
        funcionario.Salario = Convert.ToDouble(Console.ReadLine());

        Console.Write("Idade: ");
        funcionario.Idade = Convert.ToInt32(Console.ReadLine());

        var errosValidacao =  new ModelPessoa().ValidarCampos(funcionario);

        if (errosValidacao.Count == 0)
        {
            Console.WriteLine("Cadastrato com sucesso.");
            Console.ReadKey();
            return;
        }
        foreach(var erro in errosValidacao) 
            Console.WriteLine(erro);
        Console.ReadKey();
    }
}

Another benefit of this validation model is that it shows the errors of all fields at once, instead of having to teach the user to fill one by one on the basis of "trial-and-error".

Also note that the validation functions Peasant and Valorminimo have become so generic that they can actually be used to validate any entity in your system and not just the official!

Just move them to a more generic class and then the Modelpessoa, and the validation code of any entity in the system would consume this generic validation class.

You can still return objects as a result of the validation instead of just returning the messages, so that you can put in the validation result more useful information, such as identifying the field so that you can mark it on the screen or display the validation message next to it if you are using a graphical user interface.

Declarative validations

Now that you have generalized validations by type of validation instead of writing a specific one for each field, a next step would be to create Attributes to declare these validations on each entity property instead of having to explicitly invoke the validation for each property to be validated.

So instead of doing so for each field:

ValidaCampoObrigatorio("Nome", funcionario.Nome, mensagem, errosValidacao);

You’d do something like this:

[Obrigatorio(mensagemPattern)]
public string Nome { get; set; }
[ValorMinimo(18, mensagemPattern)]
public int Idade { get; set; }

And the method Modelpessoa.Validarcampos would have a single line, invoking this validation based on Attributes, which, of course, would also be generic for any type of entity in the system:

public List<string> ValidarCampos(Pessoa funcionario)
{
    return new ValidacaoEntidade(mensagensPattern).Valida(funcionario);
}

It is logical that in this case probably the class Modelpessoa should disappear and the validation method would be in the entity itself.

You will probably also want to use a file Resources for error messages instead of leaving them hardcoded; not for the need to create translations for the system because you may never need to, but rather to facilitate programming, simplify code and centralize system messages, facilitating change and reuse.

Example of declarative validations

When using Attributes to declare the validation of each field, I killed your class Modelpessoa because her only function was to validate the entity Person, and now this function remains with the class Validity, which is able to validate any entity of the system and not just people.

interface IValidacaoAttribute
{
    string Mensagem { get; }
    bool Valido(object valor);
}

class ObrigatorioAttribute : Attribute, IValidacaoAttribute
{
    public string Mensagem { get; set; }

    public bool Valido(object valor)
    {
        return valor != null && !((valor as string) == "");
    }
}

class ValorMinimoAttribute : Attribute, IValidacaoAttribute
{
    public double Minimo { get; set; }
    public string Mensagem { get; set; }

    public bool Valido(object valor)
    {
        return valor != null 
            && (double)Convert.ChangeType(valor, typeof(double)) >= Minimo;
    }
}

class ValidacaoEntidade
{
    public static List<string> Valida<T>(T entidade)
    {
        var errosValidacao = new List<string>();
        var propriedades = typeof(T).GetProperties()
            .Where(prop => prop.IsDefined(typeof(IValidacaoAttribute), false));

        foreach (var propriedade in propriedades)
        {
            var validacoes = (IValidacaoAttribute[])propriedade
                .GetCustomAttributes(typeof(IValidacaoAttribute), false);
            foreach (var validacao in validacoes)
            {
                if (!validacao.Valido(propriedade.GetValue(entidade)))
                    errosValidacao.Add(string.Format(validacao.Mensagem));
            }
        }
        return errosValidacao;
    }
}

class Pessoa
{
    [Obrigatorio(Mensagem = "Nome é obrigatório.")]
    public string Nome { get; set; }

    [Obrigatorio(Mensagem = "Endereço é obrigatório.")]
    public string Endereco { get; set; }

    [Obrigatorio(Mensagem = "Cargo é obrigatório.")]
    public string Cargo { get; set; }

    [ValorMinimo(Minimo = 18, Mensagem = "Funcionário tem que ser adulto")]
    public int Idade { get; set; }

    [Obrigatorio(Mensagem = "Salário é obrigatório.")]
    [ValorMinimo(Minimo = 0.01, Mensagem = "Salário deve conter valor acima de zero.")]
    public double? Salario { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        Pessoa funcionario = new Pessoa();

        Console.Write("Nome: ");
        funcionario.Nome = Console.ReadLine();

        Console.Write("Endereço: ");
        funcionario.Endereco = Console.ReadLine();

        Console.Write("Cargo: ");
        funcionario.Cargo = Console.ReadLine();

        Console.Write("Salario: ");
        double salario;
        funcionario.Salario =
            double.TryParse(Console.ReadLine(), out salario) ? (double?)salario : null;

        Console.Write("Idade: ");
        funcionario.Idade = Convert.ToInt32(Console.ReadLine());

        var errosValidacao = ValidacaoEntidade.Valida<Pessoa>(funcionario);

        if (errosValidacao.Count == 0)
        {
            Console.WriteLine("Cadastrato com sucesso.");
            return;
        }
        foreach (var erro in errosValidacao)
            Console.WriteLine(erro);
    }
}

Note that each field may have more than one type of validation (in the field Salary I used two validations).

To more complex validations where not to use Attributes, you could have your entities implement an interface with a method Validate and call this method during entity validation. Within this method you could write any validation code and add messages to the list of validation errors.

Although I consider the validator implementation based on Attributes quite simple, see that no matter the complexity of it because one of the great benefits is precisely to move away from the validation complexity of business logic and application.

Of course this example is based only on the need you have exposed. In practice, you will need many other types of validation; and here we see one more advantage of the declarative style: for each new type of validation, you just need to implement a new attribute class (in the same way as Obligatory and Valorminimo) and declare these validations on any property you wish, of any entity. You will not have to mess with any existing class.

You can implement each of them on your own, as I have shown here in two examples, and you can also use frameworks to save you service (usually validation libraries are used in association with ORM frameworks).

Completion

You can improve your code by simply creating functions to generalize validations that are already conceptually generic. And generalizing code is an important practice in any area of the system, not just in validations.

You can go further by creating Attributes for declare the validations instead of writing them in an imperative way for each entity of the system.

You can also use frameworks who have already implemented validation standards that serve their project.

And if you opt for a framework, the cool thing is that if you’ve assimilated this answer, you’ll already know in part how they work instead of just seeing magic, and this kind of knowledge can help you in many ways.

  • 2

    If you are interested in a functional example using Attributes just leave a comment.

  • 1

    I will study your answer further and implement your solution, and yes I am interested in the example using Ttributes, if you have anything else to add.

  • 1

    @All right, I posted the code.

  • I implemented the two examples of you, but I have some more questions I’ll see if I ask more questions, thanks for the help :) +++++1.

3

We will improve the reading of your code so we will ignore the whole numbers and use numerators, as I did below:

public enum ResultadoValidacao : int
{
    nomeNulo = 1,
    enderecoNulo = 2,
    cargoNulo = 4,
    salarioInvalido = 8,
    menorDeIdade = 16,
    tudoOk = 32  // se estiver tudo beleza
};

public ResultadoValidacao InvaValidarCampos(Pessoa funcionario) 
{
    if (string.IsNullOrEmpty(funcionario.Nome)) //Verifica se há conteúdo na variável.
        return ResultadoValidacao.nomeNulo;
    if (string.IsNullOrEmpty(funcionario.Endereco)) //Verifica se há conteúdo na variável.
        return ResultadoValidacao.enderecoNulo;
    if (string.IsNullOrEmpty(funcionario.Cargo)) //Verifica se há conteúdo na variável.
        return ResultadoValidacao.cargoNulo;
    if (funcionario.Salario <= 0) //Verifica se o valor do salário é valido.
        return ResultadoValidacao.salarioInvalido;
    if (funcionario.Idade < 18) //Verifica se o funcionário é maior de idade.
        return ResultadoValidacao.menorDeIdade;

    /* você pode usar números também, como por exemplo, menorDeIdade = 16 */
    return ResultadoValidacao.tudoOk; //Todos os campos atende os requisitos das regras.
}

Now let’s simplify this part of the code by implementing the function EscreverEObter; declare this method:

string EscreverEObter(string oQuePedir)
{
    Console.Write("\n" + oQuePedir);
    return Console.ReadLine();
}

And we will use this method in your code:

Pessoa funcionario = new Pessoa();
ModelPessoa mp = new ModelPessoa();

funcionario.Nome = EscreverEObter(@"Nome: ");
funcionario.Endereco = EscreverEObter(@"Endereço: ");
funcionario.Cargo= EscreverEObter(@"Cargo: ");
funcionario.Salario = EscreverEObter(@"Salário: ");
funcionario.Idade = EscreverEObter(@"Idade: ");

//Tem duas variáveis que tem que ser apenas numéricas, vamos ver se o usuário
//digitou um número válido nela.
if (!Decimal.TryParse(funcionário.Salario, null)) { //Decimal por que o salário é um decimal
    Console.WriteLine("Salário inválido.");
}
if (!UInt16.TryParse(funcionário.Idade, null)) { //Idade é um número inteiro
    Console.WriteLine("Idade inválida.");
}

switch (mp.ValidarCampos(funcionario)) 
{
    case ResultadoValidacao.nomeNulo:
        Console.WriteLine("Nome é obrigatório.");
        break;
    case ResultadoValidacao.enderecoNulo:
        Console.WriteLine("Endereço é obrigatório");
        break;
    case ResultadoValidacao.cargoNulo:
        Console.WriteLine("Cargo é obrigatório.");
        break;
    case ResultadoValidacao.salarioInvalido:
        Console.WriteLine("Salario deve conter valor acima de zero.");
        break;
    case ResultadoValidacao.menorDeIdade:
        Console.WriteLine("Funcionário tem que ser adulto.");
        break;
    default: 
        Console.WriteLine("Cadastrado com sucesso."); 
        break;
}

There are many ways to simplify your code, one of them is by using regular expressions to validate a text.

  • 1

    Thank you for sharing your knowledge.

  • 1

    The case ResultadoValidacao.cargoNulo: didn’t work, I had to trade for case ValidacaoCampo.Pessoa.ResultadoValidacao.NomeNulo: Because the Compiler asked for it and didn’t accept it. PS: Validatelocation is the namespace of my example.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.