ASP.Net Core : Le petit souci de [Required]

L'attribut [Required] permet d'indiquer qu'un paramètre est requis en entrée d'une action de contrôleur. Seulement, cet attribut a une limitation qui est frustrante.

Parterre de Gentiane
Parterre de Gentiane dans les calanques de Marseille

L'attribut [Required] permet d'assurer la présence d'un paramètre en entrée d'une action d'un contrôleur :

[ApiController]
[Route("data")]
public class DataController : ControllerBase
{
    [HttpGet]
    public IActionResult Get([Required] int id, [Required] string value)
    {
        return Ok(new { id, value });
    }
}

Ainsi si un paramètre requis est omis, l'application Web retourne une erreur 400 (Bad Request) :

> curl http://localhost:5000/data
HTTP response status code 400 - Bad Request
Content :
{
    "errors": {
        "id": [
            "The id field is required."
        ],
        "value": [
            "The value field is required."
        ]
    }
}

Seulement, l'attribut [Required] a une limitation avec les types complexes :

[ApiController]
[Route("data")]
public class DataController : ControllerBase
{
    [HttpGet]
    public IActionResult Get([FromQuery] Data data)
    {
        return Ok(data);
    }

    public class Data
    {
        [Required]
        public int Id { get; set; }
        [Required]
        public string Value { get; set; }
    }
}

Si on applique la même requête, on obtient un résultat différent :

> curl http://localhost:5000/data
HTTP response status code 400 - Bad Request
Content :
{
    "errors": {
        "value": [
            "The value field is required."
        ]
    }
}

L'application Web retourne bien une erreur 400 (Bad Request), mais seul l'absence du paramètre Value est indiquée. L'absence du paramètre requis Id n'est pas signalée. Par conséquent, une requête sans le paramètre requis Id (donc invalide) est acceptée :

> curl http://localhost:5000/data?Value=Hello
HTTP response status code 200 - OK
Content :
{
    "id": 0,
    "value": "Hello"
}

Le problème vient du fonctionnement de la validation des paramètres d'entrée de type complexe. Dans ce cas, une instance du type en entrée est créée et ses propriétés sont initialisées avec une valeur par défaut (fonctionnement standard en .NET). Ensuite survient la validation, qui vérifie que les propriétés de l'instance avec [Required] ont une valeur différente de nulle. Le problème étant que les propriétés de type non-nullable ne peuvent avoir la valeur nulle, donc elles seront toujours différente de nulle et l'attribut [Required] est sans effet.

Si vous avez travaillé avec le classique ASP.NET, vous devez connaître le contournement à ce problème. Il faut que les propriétés ayant l'attribut [Required] soit d'un type nullable :

[ApiController]
[Route("data")]
public class DataController : ControllerBase
{
    [HttpGet]
    public IActionResult Get([FromQuery] Data data)
    {
        return Ok(data);
    }

    public class Data
    {
        [Required]
        public int? Id { get; set; }
        [Required]
        public string Value { get; set; }
    }
}

Dans ce cas, on retrouve le comportement désiré :

> curl http://localhost:5000/test
HTTP response status code 400 - Bad Request
Content :
{
    "errors": {
        "Id": [
            "The Id field is required."
        ],
        "Value": [
            "The Value field is required."
        ]
    }
}

Ce contournement, en plus d'être inélégant, a un défaut particulièrement pénible. La propriété, étant requise, ne devrait pas pouvoir être nulle, donc on devrait pouvoir l'utiliser directement sans danger. Seulement les outils d'analyse ne le savent pas et vont générer des avertissements inutiles. Inutiles... Pas vraiment et on va tout de même ajouter les vérifications nécessaires... Au cas où!

Voilà le petite souci avec l'attribut [Required] en ASP.NET Core. Je sais que ce n'est qu'un petit détail, mais comme l'indique le proverbe :

Le diable est dans les détails