ValidationAttribute personalizado e validação Blazor

· 4 min de leitura

# Personalize tudo

Como você provavelmente já viu em todos os meus posts, eu realmente tento manter tudo o mais limpo possível, pois já escrevi posts sobre atributos customizados, tratamento de exceções customizadas, injeção de coleção de serviços, etc.

Com o tempo, percebi que esse tipo de codificação dá a mim e à minha equipe uma maneira de melhorar as horas extras, encontrar problemas com mais facilidade e separar o código o máximo possível.

Sim, depois dessa história legal que acabei de contar, tenho trabalhado no Blazor ultimamente como de costume e descobri que, depois de anos e anos de desenvolvimento, você pode criar atributos de validação personalizados.

Sim, é engraçado, depois de todos esses anos…

Atributos de validação personalizados

A ideia veio do trabalho, na verdade, sempre fazemos validação em todos os lugares, mas eu tinha alguns campos que exigiam o mesmo processo de validação, então pensei que poderia haver algo lá… como atributos de validação customizados!

Então, abri a documentação da Microsoft para isso e descobri que sim, você pode criar atributos de validação personalizados e atribuí-los às propriedades, assim:

public class StringLengthRangeAttribute : ValidationAttribute
{
    public int Minimum { get; set; }
    public int Maximum { get; set; }

    public StringLengthRangeAttribute()
    {
        this.Minimum = 0;
        this.Maximum = int.MaxValue;
    }

    public override bool IsValid(object value)
    {
        string strValue = value as string;
        if (!string.IsNullOrEmpty(strValue))
        {
            int len = strValue.Length;
            return len >= this.Minimum && len <= this.Maximum;
        }
        return true;
    }
}

e use-o em uma classe simples como esta:

[Required]
[StringLengthRange(Minimum = 10, ErrorMessage = "Must be >10 characters.")]

[StringLengthRange(Maximum = 20)]

[Required]
[StringLengthRange(Minimum = 10, Maximum = 20)]

Validador personalizado

Então eu tenho esse validador que preciso para algum caso de negócio específico que contará 20 primeiros caracteres que serão 9 números e um hífen, e terminarão com 2 caracteres que normalmente serão o código do país, então algo assim: 123456789-123456789-ES

Acabei chegando com algo assim, é muito simples, mas funciona:

using System;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;

public class SpecialStringValidatorAttribute : ValidationAttribute
{
    private const int TotalLength = 22;
    private const string Pattern = @"^(\d{10})-(\d{10})-([A-Za-z]{2})$";

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        string strValue = value as string;

        if (!string.IsNullOrEmpty(strValue))
        {
            if (strValue.Length != TotalLength)
            {
                return new ValidationResult($"The string must be {TotalLength} characters long.");
            }

            if (!Regex.IsMatch(strValue, Pattern))
            {
                return new ValidationResult("The string must follow the pattern: 1234567890-1234567890-AB");
            }

            return ValidationResult.Success;
        }

        return new ValidationResult("The string cannot be null or empty.");
    }
}

Testes

Eu escrevi alguns testes para eles também, só para garantir:

public class SpecialStringValidatorTests
{
    [Theory]
    [InlineData("1234567890-1234567890-AB", true)]
    [InlineData("1234567890-1234567890-XY", true)]
    [InlineData("1234567890-1234567890-A1", false)]
    [InlineData("1234567890-1234567890-A", false)]
    [InlineData("1234567890-123456789-AB", false)]
    [InlineData("1234567890-1234567890", false)]
    [InlineData("1234567890-1234567890-ABCDE", false)]
    public void SpecialStringValidatorTest(string input, bool expectedResult)
    {
        // Arrange
        var validator = new SpecialStringValidatorAttribute();

        // Act
        var result = validator.IsValid(input);

        // Assert
        Assert.Equal(expectedResult, result);
    }
}

E quando executei tive estes resultados:

Microsoft (R) Test Execution Command Line Tool Version 16.9.1
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...

A total of 1 test files matched the specified pattern.

Test run in progress...

Passed!  - SpecialStringValidatorTests.SpecialStringValidatorTest(input: "1234567890-1234567890-AB", expectedResult: True)
Passed!  - SpecialStringValidatorTests.SpecialStringValidatorTest(input: "1234567890-1234567890-XY", expectedResult: True)
Passed!  - SpecialStringValidatorTests.SpecialStringValidatorTest(input: "1234567890-1234567890-A1", expectedResult: False)
Passed!  - SpecialStringValidatorTests.SpecialStringValidatorTest(input: "1234567890-1234567890-A", expectedResult: False)
Passed!  - SpecialStringValidatorTests.SpecialStringValidatorTest(input: "1234567890-123456789-AB", expectedResult: False)
Passed!  - SpecialStringValidatorTests.SpecialStringValidatorTest(input: "1234567890-1234567890", expectedResult: False)
Passed!  - SpecialStringValidatorTests.SpecialStringValidatorTest(input: "1234567890-1234567890-ABCDE", expectedResult: False)

Test Run Successful.
Total tests: 7
     Passed: 7
 Total time: 1.7296 Seconds

Validação personalizada e Blazor

Então agora que sei que o dele pode ser usado, é claramente uma boa ideia implementá-lo no Blazor, certo?

Vamos supor que eu tenha este formulário, que usará o modelo Person que mostrei antes:

@using Models
@page "/"

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

<EditForm Model=@Person FormName="PersonForm">
  <DataAnnotationsValidator/>
  <ValidationSummary/>
  <div class="form-group">
    <label for="Name">Name</label>
    <InputText @bind-Value=Person.Name class="form-control" id="Name" />
    <ValidationMessage For="() => Person.Name"/>
  </div>
  <div class="form-group">
    <label for="MySpecialString">My special string</label>
    <InputText @bind-Value=Person.MySpecialString class="form-control" id="Name" />
    <ValidationMessage For="() => Person.MySpecialString"/>
  </div>
  <div class="form-group">
    <label for="Age">Age</label>
    <InputNumber @bind-Value=Person.Age class="form-control" id="Age" />
    <ValidationMessage For=@(() => Person.Age) />
  </div>
  <input type="submit" class="btn btn-primary" value="Save"/>
</EditForm>

@code {
  Person Person = new Person();
}

Assim que executamos isso, obtemos os seguintes erros:

E se colocarmos apenas o que queremos, obtemos o seguinte, tudo claro:

Para ser honesto, está bastante claro que devemos mover a lógica pelo menos para validar esses formulários em atributos de validação personalizados, isso nos dá liberdade para armazenar o código desse login em um único local e podemos usá-lo posteriormente para uma API ou outro aplicativo.

Espero que tenham gostado, se tiver alguma dúvida ou quiser entrar em contato, não hesite e entre em contato comigo!