Prática recomendada para usar tipos de referência nulos para DTOs

20

Eu tenho um DTO que é preenchido pela leitura de uma tabela do DynamoDB. Diga que parece com isso atualmente:

public class Item
{
    public string Id { get; set; } // PK so technically cannot be null
    public string Name { get; set; } // validation to prevent nulls but this doesn't stop database hacks
    public string Description { get; set; } // can be null
}

Existe alguma prática recomendada para lidar com isso? Prefiro evitar um construtor sem parâmetros, pois isso é ruim com o ORM no Dynamo SDK (assim como com outros).

Parece-me estranho escrever public string Id { get; set; } = "";porque isso nunca acontecerá, pois Idé um PK e nunca pode ser nulo. De que serviria ""mesmo que de alguma maneira?

Então, alguma prática recomendada sobre isso?

  • Devo marcar todos eles string?para dizer que podem ser nulos, embora alguns nunca devam ser?
  • Devo inicializar Ide Namecom, ""porque eles nunca devem ser nulos e isso mostra a intenção, mesmo que ""nunca fosse usada.
  • Alguma combinação de acima

Observe: trata-se de tipos de referência anuláveis ​​em C # 8 Se você não souber o que é melhor, não responda.

BritishDeveloper
fonte
É um pouco sujo, mas você pode simplesmente dar um tapa #pragma warning disable CS8618na parte superior do arquivo.
20
7
Em vez disso = "", você pode = null!inicializar uma propriedade que você sabe que nunca será efetivamente null(quando o compilador não tem como saber disso). Se Descriptionlegalmente pode ser null, deve ser declarado a string?. Como alternativa, se a verificação de nulidade do DTO for mais incômoda do que ajuda, você pode simplesmente envolver o tipo #nullable disable/ #nullable restoredesativar os NRTs somente para esse tipo.
Jeroen Mostert
@JeroenMostert Você deve colocar isso como resposta.
Magnus
3
@ Magnus: Estou relutante em responder qualquer pergunta pedindo "melhores práticas"; essas coisas são amplas e subjetivas. Espero que o OP possa usar meu comentário para desenvolver suas próprias "melhores práticas".
Jeroen Mostert
11
@ IvanGarcíaTopete: Embora eu concorde que o uso de uma string para uma chave primária seja incomum e possa até ser desaconselhável, dependendo das circunstâncias, a escolha do tipo de dados do OP é bastante irrelevante para a questão. Isso poderia ser aplicado facilmente a uma propriedade de string obrigatória e não anulável que não é a chave primária, ou mesmo a um campo de string que faz parte de uma chave primária composta, e a pergunta ainda permanece.
Jeremy Caney

Respostas:

12

Como opção, você pode usar o defaultliteral em combinação com onull forgiving operator

public class Item
{
    public string Id { get; set; } = default!;
    public string Name { get; set; } = default!;
    public string Description { get; set; } = default!;
}

Como seu DTO é preenchido no DynamoDB, você pode usar MaybeNull/NotNull atributos pós-condição para controlar a nulidade

  • MaybeNull Um valor de retorno não anulável pode ser nulo.
  • NotNull Um valor de retorno anulável nunca será nulo.

Mas esses atributos afetam apenas a análise anulável para os chamadores dos membros que são anotados com eles. Normalmente, você aplica esses atributos aos retornos de métodos, propriedades e getters de indexadores.

Portanto, você pode considerar todas as suas propriedades não anuláveis ​​e decorá-las com MaybeNullatributo, indicando que elas retornam um nullvalor possível

public class Item
{
    public string Id { get; set; } = "";
    [MaybeNull] public string Name { get; set; } = default!;
    [MaybeNull] public string Description { get; set; } = default!;
}

O exemplo a seguir mostra o uso da Itemclasse atualizada . Como você pode ver, a segunda linha não mostra aviso, mas a terceira

var item = new Item();
string id = item.Id;
string name = item.Name; //warning CS8600: Converting null literal or possible null value to non-nullable type.

Ou você pode tornar todas as propriedades anuláveis ​​e usar NoNullpara indicar que o valor de retorno não pode ser null( Idpor exemplo)

public class Item
{
    [NotNull] public string? Id { get; set; }
    public string? Name { get; set; }
    public string? Description { get; set; }
}

O aviso será o mesmo com o exemplo anterior.

Também existem AllowNull/DisallowNull atributos de pré-condição para parâmetros de entrada, propriedades e configuradores de indexadores, trabalhando da mesma maneira.

  • AllowNull Um argumento de entrada não anulável pode ser nulo.
  • DisallowNull Um argumento de entrada anulável nunca deve ser nulo.

Eu não acho que isso irá ajudá-lo, já que sua classe é preenchida a partir do banco de dados, mas você pode usá-los para controlar a nulidade dos configuradores de propriedades, como este para a primeira opção

[MaybeNull, AllowNull] public string Description { get; set; }

E para o segundo

[NotNull, DisallowNull] public string? Id { get; set; }

Alguns detalhes e exemplos úteis de post / pré-condições podem ser encontrados neste artigo do devblog

Pavel Anikhouski
fonte
6

A resposta do livro neste cenário é usar a string?para sua Idpropriedade, mas também decorá-la com o [NotNull]atributo:

public class Item
{
  [NotNull] public string? Id { get; set; }
  public string Name { get; set; }
  public string? Description { get; set; }
}

Referência: de acordo com a documentação , o [NotNull]atributo "especifica que uma saída não é nula, mesmo que o tipo correspondente permita".

Então, o que exatamente está acontecendo aqui?

  1. Em primeiro lugar, o string?tipo de retorno impede o compilador de avisando que a propriedade é inicializado durante a construção e, assim, o padrão para null.
  2. Em seguida, o [NotNull]atributo evita um aviso ao atribuir a propriedade a uma variável não anulável ou ao tentar desreferenciá-la, pois você está informando à análise de fluxo estático do compilador que, na prática , essa propriedade nunca será null.

Aviso: Como em todos os casos que envolvem o contexto de nulidade do C #, não há nada tecnicamente impedindo que você ainda retorne um nullvalor aqui e, portanto, potencialmente, introduzindo algumas exceções posteriores; ou seja, não há validação de tempo de execução pronta para uso. Tudo o que o C # fornece é um aviso do compilador. Ao apresentar, [NotNull]você efetivamente substitui esse aviso, dando uma dica sobre sua lógica de negócios. Assim, quando você anota uma propriedade [NotNull], você assume a responsabilidade pelo seu compromisso de que "isso nunca acontecerá, pois Idé um PK e nunca pode ser nulo".

A fim de ajudar a manter esse compromisso, você pode também querer anotar a propriedade com o [DisallowNull]atributo:

public class Item
{
  [NotNull, DisallowNull] public string? Id { get; set; }
  public string Name { get; set; }
  public string? Description { get; set; }
}

Referência: de acordo com a documentação , o [DisallowNull]atributo "especifica que nullnão é permitido como uma entrada, mesmo que o tipo correspondente permita".

Isso pode não ser relevante no seu caso, uma vez que os valores estão sendo atribuídos por meio do banco de dados, mas o [DisallowNull]atributo emitirá um aviso se você tentar atribuir um nullvalor (capaz) Id, mesmo que o tipo de retorno permita que ele seja nulo . Nesse sentido, Idagiria exatamente como a no stringque diz respeito à análise de fluxo estático do C #, além de permitir que o valor permanecesse não inicializado entre a construção do objeto e a população da propriedade.

Nota: Como já mencionado, você também pode obter um resultado praticamente idêntico atribuindo o Idvalor padrão de um default!ou null!. É certo que essa é uma preferência estilística. Prefiro usar as anotações de nulidade, pois elas são mais explícitas e fornecem controle granular, enquanto é fácil abusar dessa !forma de desligar o compilador. A inicialização explícita de uma propriedade com um valor também me incomoda, se eu souber que nunca vou usar esse valor - mesmo que seja o padrão.

Jeremy Caney
fonte
-2

String é um tipo de referência e sempre anulável, você não precisa fazer nada de especial. Você poderá ter problemas apenas mais tarde se desejar mapear esse tipo de objeto para outro, mas poderá lidar com isso mais tarde.

dino
fonte
8
Ele está falando sobre tipos de referência Nullable em C # 8
Magnus