Validación personalizada de atributos y validación de Blazor

· 4 min de lectura

# Personalizar todo

Como probablemente habrás visto en todas mis publicaciones, realmente trato de mantener todo lo más limpio posible, ya que ya escribí publicaciones sobre atributos personalizados, manejo de excepciones personalizado, inyección de recopilación de servicios, etc.

Con el tiempo me di cuenta de que este tipo de codificación nos brinda a mí y a mi equipo una manera de mejorar con el tiempo, encontrar problemas más fácilmente y separar el código tanto como sea posible.

Sí, después de esta interesante historia que les acabo de contar, últimamente he estado trabajando en Blazor como de costumbre y descubrí que, después de años y años de desarrollo, se pueden crear atributos de validación personalizados.

Sí, es gracioso, después de todos estos años…

Atributos de validación personalizados

En realidad, la idea surgió del trabajo, siempre hacemos validación en todos los lugares, pero tenía algunos campos que requerían el mismo proceso de validación, así que pensé que podría haber algo allí… ¡como atributos de validación personalizados!

Así que abrí la documentación de Microsoft para eso y descubrí que sí, puedes crear atributos de validación personalizados y asignarlos a las propiedades, así:

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;
    }
}

y usarlo en una clase simple como esta:

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

[StringLengthRange(Maximum = 20)]

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

Validador personalizado

Así que tengo este validador que necesito para algún caso de negocio específico que contendrá 20 primeros caracteres, de los cuales 9 números y un guión, y terminará con 2 caracteres que generalmente serán el código de país, algo como esto: 123456789-123456789-ES

Terminé con algo como esto, es muy simple pero 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.");
    }
}

Pruebas

También escribí algunas pruebas para ellos, por si acaso:

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);
    }
}

Y cuando ejecuté obtuve estos 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

Validación personalizada y Blazor

Ahora que sé que se puede usar, es claramente una buena idea implementarlo en Blazor, ¿verdad?

Supongamos que tengo este formulario, que usará el modelo Person que mostré 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();
}

Tan pronto como ejecutamos esto, obtenemos los siguientes errores:

Y si simplemente ponemos lo que queremos, obtenemos lo siguiente, todo claro:

Para ser honesto, está bastante claro que debemos mover la lógica al menos para validar esos formularios a atributos de validación personalizados, nos da libertad para almacenar el código para ese inicio de sesión en un solo lugar y podemos usarlo más tarde para una API u otra aplicación.

Espero que te haya gustado, si tienes alguna duda o quieres ponerte en contacto, ¡no lo dudes y contacta conmigo!