É comum na modelagem de banco de dados termos colunas com valores nulos. Para tipos como string, que são "reference type", valor null é extremamente comum, mas e quanto a valores primitivos como: int, float, double, DateTime (struct no C#) e entre outros?

Na versão 2.0 do .NET, sim numa versão já adotada no mercado (.NET 1.1 é bem pouco, espero) apresentou o conceito de Nullable Types que permite você inferir valores nulos à built-in types (ou tipos primitos). Não irei entrar em detalhes muito profundos, pois pretendo ser breve nest post.

A sintaxe é bem simples:

int? a = null;

// dois properties para Nullable Types, HasValue e Value
if (a.HasValue) Console.WriteLine(a.Value);

Assim podemos atribuir valores para nulos e não mais valores default para estes tipos de dados.

Ok, isso é bonito mas o IDataReader não possui um método para tratar quando valores são nulos, o que fazemos? Realizamos uma checagem com o método IsDbNull, e se for podemos retornar nulo ou o valor caso OK. Veja abaixo:

int? coluna = reader.IsDBNull(i) ? (int?)null : (int?)reader.GetInt32(i);

Isso resolveria o nosso problema, mas e se tivermos 10 colunas na tabela, esse código polui um pouco, não? Pena que o GetInt32(int i) não lida com campos nulos.

Como podemos melhorar isso?

Podemos usar Extension Methods!

Num post anterior eu cheguei a falar bem básicamente sobre extension methods, veja como resolveríamos este problema:

static class DataReaderTypeHelper
{
  public static int? GetNullOrInt32(this IDataReader reader, int index)
  {
    return reader.IsDBNull(index) ? (int?)null : (int?)reader.GetInt32(index);
  }

  public static string GetNullOrString(this IDataReader reader, int index)
  {
    return reader.IsDBNull(index) ? null : reader.GetString(index);
  }

  public static DateTime? GetNullOrDateTime(this IDataReader reader, int index)
  {
    return reader.IsDBNull(index) ? (DateTime?)null : (DateTime?)reader.GetDateTime(index);
  }
}

Agora podemos este método e tornar a coisa mais bonita e direta:

int? coluna = reader.GetNullOrInt32(index);

Muito mais simples, fácil e direto de usar. Podemos fazer também para inserção, remoção ou atualização onde valores nulos encrencam no banco de dados também. Normalmente teríamos que fazer:

cmd.Parameters.AddWithValue("@param1", param1);
cmd.Parameters.AddWithValue("@param2", param2);

Se usassemos o código acima, se @param2 pudesse ser nulo no banco de dados, este código geraria uma SqlException caso o valor de param2 fosse nulo, pois o null do c# é diferente do null para o banco de dados, então teríamos que verificar o valor e assim retornar DBNull.Value.

Com Nullable Types, isso ficaria extremamente simples, nada de if's:

cmd.Parameters.AddWithValue("@param2", (object)param2 ?? DBNull.Value);

Observe que temos de fazer um cast para object em param2, pois para o compilador o operador ?? deve garantir que param2 e DBNull.Value sejam do mesmo tipo, por que Nullable usa Generics para inferir tipos, embora mascare isso.

Usando novamente Extension Methods podemos fazer uma classe que nos ajude nisso:

static class DbHelper
{
  public static void AddWithValueOrNull(this SqlParameterCollection collection, string param, object value)
  {
    collection.AddWithValue(param, (object)value ?? DBNull.Value);
  }
}

Agora temos um método AddWithValueOrNull para o Parameters que é do tipo SqlParametersCollection. Assim encapsulamos tudo numa sintaxe mais limpa:

cmd.Parameters.AddWithValueOrNull("@param2", param2);

Nota usamos este método apenas para valores que possam ser nulos e não para todos, no caso de chaves primárias devemos usar o AddWithValue mesmo por que devemos reforçar a necessidade de valores não nulos.