O número inclui uma política de cálculo
decimal permite representar exatamente valores como 1.005m, mas não escolhe a regra de arredondamento da aplicação. Especifica a precisão, o modo e o momento do cálculo. Math.Round(2.345m, 2, MidpointRounding.ToEven) resolve o empate para 2.34; AwayFromZero escolhe 2.35.
Fora de um empate, estes dois modos escolhem o valor mais próximo. Arredondar cada parcela antes de somar pode produzir um total diferente de arredondar uma única vez no fim. Duas parcelas de 1.005m dão 2.02m com arredondamento individual AwayFromZero, mas 2.01m se forem somadas primeiro.
Nenhuma ordem é universalmente correta: o contrato do relatório decide. Conserva os valores originais e a política usada para permitir reconciliação; formatação para duas casas não corrige uma soma calculada por outra regra.
Interpretar texto com cultura explícita
Um protocolo de dados não deve depender da cultura da máquina que o processa. Define o separador decimal e os elementos permitidos. Decimal.TryParse com CultureInfo.InvariantCulture e NumberStyles.AllowDecimalPoint aceita o ponto decimal, mas não autoriza vírgulas de agrupamento nem sinais se esses estilos não forem acrescentados.
A string "12,50" deve ser rejeitada nesse contrato, não reinterpretada silenciosamente. NumberStyles.Number é mais permissivo e permite separadores de milhares; escolhê-lo sem ler o contrato pode transformar uma entrada mal formatada noutro número válido.
Verifica primeiro o Boolean de TryParse e depois os limites de domínio. Um número sintaticamente válido pode ser negativo, excessivo ou ter uma precisão incompatível com o produto. Não uses o valor out de uma conversão falhada como se fosse uma medição real.
Data civil, instante e recorrência
Escolhe o tipo segundo o significado do dado. Uma data de início sem hora pode ser DateOnly. Um evento que ocorreu num instante pode usar DateTimeOffset com um offset conhecido. 10:00+01:00 e 09:00+00:00 representam o mesmo instante; Equals compara essa equivalência, não a igualdade da representação.
Uma regra "09:00 locais em cada dia útil" precisa de mais informação: região/fuso, calendário e decisão para horas locais ambíguas ou inexistentes. Um offset observado hoje não descreve as transições futuras da região. Separar o horário civil da ocorrência calculada evita confundir uma duração de 24 horas com o próximo dia de trabalho à mesma hora local.
Ao guardar dados, conserva informação suficiente para responder à pergunta do consumidor, incluindo a origem quando necessária para interpretar o tempo.
Rotular uma hora não é convertê-la
DateTime.SpecifyKind cria um valor com os mesmos ticks e outro Kind. Não desloca uma hora para conservar o instante noutra referência temporal. Se a origem declara que 10:00 tinha offset +02:00, atribuir Kind.Utc conserva 10:00 e passa a indicar o instante errado; a correspondência UTC seria 08:00.
O dado de origem pode trazer um offset explícito ou precisar de um fuso e de uma política para ambiguidades. Não adivinhes essa informação pela localização do servidor. De forma semelhante, formatar uma data com uma letra Z não prova que foi convertida corretamente.
Na revisão, acompanha três coisas separadas: o valor de relógio recebido, a informação temporal que o interpreta e a operação usada para obter o instante pretendido.
JSON: nomes e presença são contratos
Com opções novas de JsonSerializerOptions, a correspondência de nomes é sensível a maiúsculas e minúsculas. Uma propriedade Count não é preenchida por count sem configuração adicional; as opções web têm outros valores predefinidos. Por isso, explicita que conjunto de opções está em uso. required pede presença no payload durante a desserialização: se Count é required int, {} falha mesmo que int tenha valor predefinido zero.
A propriedade fornecida também tem de ser compatível com o tipo. Por predefinição, membros desconhecidos são ignorados; UnmappedMemberHandling.Disallow permite rejeitá-los quando o protocolo exige um esquema fechado. Esta decisão afeta evolução: um novo campo pode ser rejeitado por um consumidor estrito.
Define a compatibilidade e a versão do contrato antes de escolher tolerância ou rejeição.
Presença, null e validade de domínio
Em .NET 10, RespectNullableAnnotations = true permite impor algumas anotações de nullability durante a desserialização. Para uma propriedade string não anulável e não obrigatória com inicializador "default", {} conserva o inicializador, mas {"Name":null} viola a anotação.
A opção não torna todas as propriedades obrigatórias. required e nullability respondem a perguntas diferentes: o membro apareceu e o valor pode ser null? Há ainda uma terceira pergunta: o valor serve para o domínio? Uma string vazia pode passar as duas primeiras verificações e continuar a ser um identificador inválido.
A aplicação precisa dessa validação explícita. A imposição de anotações também tem limites, incluindo alguns contextos genéricos e elementos de coleções; não a trates como uma validação completa de todos os objetos de um grafo.
Leitura guiada: uma fronteira de entrada explícita
O exemplo recebe JSON de tamanho limitado com Id string e Amount numérico. Exige os dois membros, rejeita nomes desconhecidos e null explícito para Id, e valida o conteúdo antes de devolver. {} falha por ausência de membros; {"Id":"A","Amount":-1} é sintaticamente válido, mas falha na regra de domínio; {"Id":"A","Amount":2.345} produz uma entrada aceite sem arredondamento implícito.
O limite de um milhão pertence apenas a este exemplo. O resultado null de uma raiz JSON null também é rejeitado explicitamente, porque a opção de anotações não impõe essa restrição na raiz. O código não é um parser de datas nem uma política de valores monetários.
Os casos de tempo e arredondamento devem aplicar os seus contratos próprios depois da aceitação dos dados relevantes.
#nullable enable
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
public sealed class MeasureInput
{
public required string Id { get; set; }
public required decimal Amount { get; set; }
}
public static class Inputs
{
public static MeasureInput Read(string json)
{
var options = new JsonSerializerOptions
{
RespectNullableAnnotations = true,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow
};
var value = JsonSerializer.Deserialize<MeasureInput>(json, options)
?? throw new JsonException("An object is required.");
if (string.IsNullOrWhiteSpace(value.Id) || value.Amount < 0m
|| value.Amount > 1_000_000m)
throw new ArgumentException("Invalid measurement.");
return value;
}
}A entrada {"Id":"A","Amount":2.345} é aceite sem alterar a precisão; Amount -1 é rejeitado pela regra explícita. Não é necessário executar o código para responder.
Armadilhas comuns
Usar a cultura da máquina para protocolos; arredondar na fase errada; confundir offset com fuso; usar SpecifyKind como conversão; assumir que required também valida o domínio.
Tópicos relacionados: Tipos, conversões e limites numéricos · Referências, nullability e texto · Exceções, recursos e ficheiros
Um valor só é útil quando a representação, o significado e as regras de aceitação estão explícitos.
Referência: Math.Round · C# 14 / .NET 10; no preview features