C# - Usando records como Ids fortemente tipados - II


 Hoje vamos continuar a mostrar como usar o tipo record para definir um identificador único(Id) fortemente tipado usado nas entidades do modelo.

Continuando o artigo anterior veremos como usar na prática um Id fortemente tipado usando um record.

Já vimos que podemos usar um tipo record para definir um identificador fortemente tipado e usá-los como identificador nas entidades.

Mas antes de usá-los na prática temos que fazer alguns ajustes. Por exemplo, uma aplicação ASP .NET Core não sabe como tratar esses Ids fortemente tipados os parâmetros de rotas ou query string,  e, assim precisamos fazer alguns ajustes.

Model binding de parâmetros de rotas e query string

Vamos supor que temos a seguinte entidade em nosso modelo:

    public record ProdutoId(int Value);
    public class Produto
    {
        public ProdutoId Id { get; set; }
        public string Nome { get; set; }
        public decimal Preco { get; set; }
    }

E que em nossa API temos o seguinte endpoint:

 [ApiController]
 [Route("api/[controller]")]
 public class ProdutosController : ControllerBase
 {
   ...

      [HttpGet("{id}")]
      public ActionResult<Product> GetProduto(ProdutoId id)
      {
          // código...
      }
}
 

Ao tentar acessar este endpoint usando um request GET /api/produto/1...  teremos a seguinte mensagem de erro:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.13",
    "title": "Unsupported Media Type",
    "status": 415,
    "traceId": "00-3600640f4e053b43b5ccefabe7eebd5a-159f5ca18d189142-00"
}

O que temos aqui é que a ASP.NET Core não sabe como converter o valor 1 na URL em uma instância ProdutoId, pois 1 é um tipo primitivo e ProdutoId não é tipo primitivo e não tem um conversor de tipo associado.

Assim a ASP.NET assume que este parâmetro deve ser lido do body do request. Mas não temos um body, pois é um pedido GET.

Então temos que implementar um conversor para converter para o tipo ProdutoId.

Abaixo temos um exemplo de código que faz isso considerando apenas a conversão para string:

 public class ProdutoIdConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) =>
            sourceType == typeof(string);

        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) =>
            destinationType == typeof(string);

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            return value switch
            {
                string s => new ProdutoId(int.Parse(s)),
                null => null,
                _ => throw new ArgumentException($"Não foi possível converter de {value} para ProdutoId",
                         nameof(value))
            };
        }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value,
                        Type destinationType)
        {
            if (destinationType == typeof(string))
            {
                return value switch
                {
                    ProdutoId id => id.Value.ToString(),
                    null => null,
                    _ => throw new ArgumentException($"Não foi possível converter {value} para string", nameof(value))
                };
            }

            throw new ArgumentException($"Não é possível converter {value ?? "(null)"}
                                  para {destinationType}", nameof(destinationType));
        }
    }

Agora basta associar este conversor com o tipo record ProdutoId usando o atributo TypeConverter:


 
[TypeConverter(typeof(ProdutoIdConverter))]
  public record ProdutoId(int Value);
     

 

E agora ao tentar acessar o endpoint usando o mesmo request GET teremos a resposta:

{
    "id": {
        "value": 1
    },
    "nome": "Caderno",
    "preco": 1.8
}

Agora funciona...

Mas note que o id aparece como um objeto JSON e outro detalhe é que tivemos que criar o código para um converter apenas ProdutoId e teríamos que fazer isso para os demais Ids, e isso, praticamente anula os benefícios em usar o tipo record com sua sintaxe concisa.

Para resolver este problema teremos que criar um conversor genérico que possa tratar com qualquer tipo Id fortemente tipado.

E estamos conversados...

Não furtareis, nem mentireis, nem usareis de falsidade cada um com o seu próximo;
Levítico 19:11

Referências:


José Carlos Macoratti