Gerar arquivos CSV no C#

Gerar CSV no C# é uma das formas mais simples de exportar dados para o Excel, e dá para fazer isso sem nenhuma biblioteca externa. Neste artigo, vamos usar implicit operators, extension methods e LINQ para criar e ler arquivos .csv de forma direta.

O exemplo parte de uma lista de heróis em memória, que poderia vir do banco de dados. Ao final, você terá um arquivo gerado em disco e também o caminho de volta, do arquivo para objetos.

O que são arquivos CSV

CSV é a sigla para Comma Separated Values, ou valores separados por vírgula. Esse tipo de dado é muito comum, e você consegue editá-lo facilmente no Excel e em outras planilhas.

Os arquivos CSV nada mais são do que arquivos de texto, com conteúdo em um formato específico e a extensão .csv. Cada linha representa um registro, e cada valor da linha fica separado por vírgula.

Sempre que falamos em exportar para o Excel, o CSV é minha primeira opção. Afinal, manipular esse tipo de arquivo é muito fácil, já que ele é apenas texto.

O formato é descrito pela RFC 4180, que define regras como a primeira linha opcional de cabeçalho e o uso de aspas duplas em campos especiais. Veja um exemplo de conteúdo.

name,phone,birthdate
Bruce Wayne,5599999999999,1984-08-12
Bruce Banner,5599999999997,1979-08-11
Peter Parker,5599999999996,1992-07-12
Tony Stark,5599999999995,1988-06-10

A primeira linha é o cabeçalho, com o nome de cada coluna. Em seguida, cada linha traz os valores na mesma ordem desse cabeçalho.

Criando o modelo para gerar CSV

Para exemplificar bem, vamos tomar como base um modelo ou entidade. Ou seja, uma classe que vem do banco de dados ou que exista de qualquer outra forma no sistema.

public class Hero
{
    public string Name { get; set; }
    public string Phone { get; set; }
    public DateTime BirthDate { get; set; }
}

Com o modelo definido, tudo que precisamos para gerar CSV é converter os valores dele para uma string, separando-os por vírgula. Depois, basta gravar essas linhas em um arquivo.

IMPORTANTE: a ordem dos campos deve seguir o cabeçalho do arquivo, que é a primeira linha. Então, se o cabeçalho define name,phone,birthdate, os dados precisam sair exatamente nessa ordem.

Usando implicit operators para gerar CSV

Em outro artigo, falamos sobre implicit operators e como eles facilitam a conversão de tipos no C#. Aqui, vamos usá-los para gerar CSV a partir dos objetos.

O primeiro operador é bem simples. Ele apenas retorna uma string com o nome, o telefone e a data de nascimento, nessa ordem, separados por vírgula.

public static implicit operator string(Hero hero)
    => $"{hero.Name},{hero.Phone},{hero.BirthDate.ToString("yyyy-MM-dd")}";

Repare que a data sai no formato yyyy-MM-dd. Assim, o arquivo não depende da cultura da máquina, e a leitura fica previsível.

A conversão de uma linha do CSV para o objeto Hero é um pouco mais complicada. Isso acontece porque temos uma data, e uma string não se converte implicitamente em DateTime.

Convertendo string em DateTime com extension methods

Existem várias soluções aqui, mas os extension methods podem ser um ótimo caminho. Eles adicionam funcionalidades a outras classes, incluindo as nativas do .NET.

Neste caso, por exemplo, podemos criar um método que adiciona à string a capacidade de se converter em um DateTime.

public static class StringExtensions
{
    public static DateTime ToDateTime(this string value)
    {
        // Espera o formato ano-mês-dia gerado pelo operador implícito
        var parts = value.Split("-");
        return new DateTime(
            int.Parse(parts[0]),
            int.Parse(parts[1]),
            int.Parse(parts[2]));
    }
}

IMPORTANTE: como o intuito aqui é apenas demonstrar a técnica, não vou entrar em detalhes como o tratamento de erro nas conversões de string para inteiro.

Com a extensão criada, todas as strings do projeto passam a ter o método ToDateTime(). Dessa forma, podemos seguir com a conversão de uma linha do CSV para o objeto.

Convertendo uma linha em objeto

Agora criamos o operador implícito inverso, que recebe uma linha e devolve um Hero. Ele divide a linha pela vírgula e usa cada posição para preencher o construtor.

public static implicit operator Hero(string line)
{
    var parts = line.Split(",");
    return new Hero(
        parts[0],
        parts[1],
        parts[2].ToDateTime());
}

Novamente, o intuito aqui é apenas demonstrar, e há vários itens a tratar nas conversões de dados. Com isso, o resultado final da classe Hero fica assim.

public class Hero
{
    public Hero(string name, string phone, DateTime birthDate)
    {
        Name = name;
        Phone = phone;
        BirthDate = birthDate;
    }

    public string Name { get; set; }
    public string Phone { get; set; }
    public DateTime BirthDate { get; set; }

    public static implicit operator string(Hero hero)
        => $"{hero.Name},{hero.Phone},{hero.BirthDate.ToString("yyyy-MM-dd")}";

    public static implicit operator Hero(string line)
    {
        var parts = line.Split(",");
        return new Hero(
            parts[0],
            parts[1],
            parts[2].ToDateTime());
    }
}

Como gerar CSV com File.WriteAllLines

Com as conversões finalizadas, nosso trabalho fica fácil. Vamos usar os recursos de leitura e escrita de arquivos que o .NET fornece para gerar CSV em disco.

Podemos começar criando a lista de heróis, do tipo Hero, como se esses valores viessem do banco de dados.

var heroes = new List<Hero>
{
    new("Bruce Wayne", "5599999999996", DateTime.Now.AddYears(-40)),
    new("Bruce Banner", "5599999999997", DateTime.Now.AddYears(-38)),
    new("Peter Parker", "5599999999996", DateTime.Now.AddYears(-18)),
    new("Tony Stark", "5599999999996", DateTime.Now.AddYears(-33)),
};

Em seguida, usamos o método File.WriteAllLines do .NET, que grava uma coleção de string no formato de linhas. O único problema é que não temos uma lista de string, e sim de Hero.

Transformando a lista com LINQ

Podemos contornar isso facilmente com LINQ. O método Select transforma uma lista do tipo A em uma lista do tipo B, e como já temos a conversão de Hero para string, tudo fica mais simples.

heroes.Select(hero => (string)hero)

Note que precisamos explicitar o tipo da conversão como (string) neste caso. Sem o cast, a lambda retornaria o próprio Hero, porque o compilador infere o tipo de retorno a partir da expressão.

Como resultado final, temos o código abaixo. Ele cria um arquivo chamado heroes.csv com a lista de heróis, um por linha.

File.WriteAllLines("heroes.csv", heroes.Select(hero => (string)hero));

O WriteAllLines aceita um IEnumerable<string>, então nem precisamos chamar ToList(). Além disso, se o arquivo já existir, o método o sobrescreve.

Executando a aplicação, teremos o CSV gerado com o seguinte conteúdo.

Bruce Wayne,5599999999996,1982-01-07
Bruce Banner,5599999999997,1984-01-07
Peter Parker,5599999999996,2004-01-07
Tony Stark,5599999999996,1989-01-07

As datas variam conforme o dia da execução, já que o exemplo usa DateTime.Now. Por isso, o seu arquivo terá valores diferentes dos mostrados aqui.

Lendo um CSV

Depois de gerar CSV, a leitura do arquivo é ainda mais simples. O .NET oferece File.ReadAllLines, que retorna um array de string, e File.ReadLines, que retorna as linhas sob demanda.

Com as linhas em mãos, o operador implícito converte cada uma para um Hero. Então, basta atribuir a linha a uma variável do tipo Hero.

var lines = File.ReadLines("heroes.csv");
foreach (var line in lines)
{
    Hero hero = line; // Conversão implícita de string para Hero
    Console.WriteLine(hero.Name);
    Console.WriteLine(hero.Phone);
    Console.WriteLine(hero.BirthDate);
    Console.WriteLine("--");
}

O resultado da execução é a conversão do arquivo .csv em objetos Hero, exibidos na tela como definimos.

Bruce Wayne
5599999999996
01/07/1982 00:00:00
--
Bruce Banner
5599999999997
01/07/1984 00:00:00
--
Peter Parker
5599999999996
01/07/2004 00:00:00
--
Tony Stark
5599999999996
01/07/1989 00:00:00
--

O formato da data no console depende da cultura configurada na máquina. Se quiser entender melhor esse comportamento, veja o artigo sobre DateTime no C# e .NET.

ReadLines ou ReadAllLines

Segundo a documentação do File.ReadLines, a diferença está no momento em que as linhas ficam disponíveis. Com ReadLines, você começa a enumerar antes de o .NET terminar de ler o arquivo inteiro.

Já o ReadAllLines carrega todo o conteúdo em um array antes de devolver qualquer linha. Portanto, em arquivos grandes, o ReadLines tende a ser mais eficiente e consome menos memória.

Adicionando cabeçalho ao gerar CSV

O exemplo anterior grava apenas os dados, sem a linha de cabeçalho. No entanto, um cabeçalho ajuda bastante quem abre o arquivo no Excel, porque cada coluna ganha um nome.

Para gerar CSV com cabeçalho, basta colocar a linha de títulos antes dos dados. O método Prepend do LINQ resolve isso em uma única chamada.

var lines = heroes
    .Select(hero => (string)hero)
    .Prepend("name,phone,birthdate");

File.WriteAllLines("heroes.csv", lines);

Na leitura, porém, o cabeçalho não pode passar pelo operador implícito. Afinal, a palavra birthdate não se converte em data, e o int.Parse lançaria uma exceção.

// Ignora a primeira linha, que é o cabeçalho
foreach (var line in File.ReadLines("heroes.csv").Skip(1))
{
    Hero hero = line;
    Console.WriteLine(hero.Name);
}

Com o Skip(1), a primeira linha fica de fora. Assim, o restante do código continua igual.

Como gerar CSV de forma assíncrona

Em aplicações web ou em serviços que processam muitos dados, bloquear a thread durante a gravação não é uma boa ideia. Por isso, o .NET também oferece o File.WriteAllLinesAsync, com a mesma ideia do método síncrono.

await File.WriteAllLinesAsync("heroes.csv", heroes.Select(hero => (string)hero));

O método aceita um CancellationToken opcional. Assim, se a operação for longa, você consegue cancelá-la, por exemplo, quando o usuário desiste da exportação.

Gravando linha a linha com StreamWriter

Quando os dados vêm do banco em grandes volumes, montar todas as linhas em memória antes de gravar pode custar caro. Nesse caso, o StreamWriter permite gravar uma linha de cada vez.

await using var writer = new StreamWriter("heroes.csv");
await writer.WriteLineAsync("name,phone,birthdate");

foreach (var hero in heroes)
{
    // Cada herói vira uma linha graças ao operador implícito
    await writer.WriteLineAsync((string)hero);
}

O await using garante que o arquivo seja fechado e descarregado no fim do bloco. Dessa forma, você não precisa chamar Flush ou Dispose manualmente.

Como gerar CSV em uma API ASP.NET Core

Muitas vezes, gerar CSV não significa gravar nada em disco. Em vez disso, o usuário clica em um botão e o navegador baixa o arquivo direto da API.

Para gerar CSV nesse cenário, basta montar o conteúdo em memória e devolvê-lo como arquivo. Nas Minimal APIs, o Results.File cuida dos cabeçalhos Content-Type e Content-Disposition.

app.MapGet("/heroes/export", (IHeroRepository repository) =>
{
    var lines = repository
        .GetAll()
        .Select(hero => (string)hero)
        .Prepend("name,phone,birthdate");

    // Junta as linhas e converte para bytes em UTF-8
    var content = string.Join(Environment.NewLine, lines);
    var bytes = Encoding.UTF8.GetBytes(content);

    return Results.File(bytes, "text/csv", "heroes.csv");
});

O tipo text/csv é o MIME type registrado pela RFC 4180. Já o terceiro parâmetro define o nome sugerido para o download.

Nesse exemplo, IHeroRepository representa qualquer fonte de dados da sua aplicação. O ponto central é que a conversão continua na classe Hero, então a API não precisa saber nada sobre o formato das colunas.

Cuidados ao gerar CSV

O exemplo deste artigo funciona bem porque os dados são simples. Contudo, ao gerar CSV com dados reais, alguns detalhes podem quebrar o arquivo.

Vírgulas, aspas e quebras de linha

Imagine um herói chamado "Stark, Tony". A vírgula no nome criaria uma coluna a mais, e a leitura com Split(",") deixaria de funcionar.

A RFC 4180 resolve isso com aspas duplas. Todo campo que contém vírgula, aspas ou quebra de linha deve ficar entre aspas, e as aspas internas são duplicadas.

public static class CsvExtensions
{
    // Envolve o valor em aspas quando ele contém caracteres especiais
    public static string ToCsvField(this string value)
    {
        if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
            return $"\"{value.Replace("\"", "\"\"")}\"";

        return value;
    }
}

Esse método resolve a escrita. Por outro lado, a leitura de campos com aspas exige um parser de verdade, porque o Split simples não entende essas regras.

Separador e codificação no Excel

Em instalações configuradas para o Brasil, o Excel costuma usar o ponto e vírgula como separador de lista, já que a vírgula é o separador decimal. Por isso, um CSV separado por vírgula pode abrir com todos os valores em uma única coluna.

Nesse caso, você pode gerar CSV com ; como separador ou usar o assistente de importação do Excel. O importante é que a escrita e a leitura usem o mesmo caractere.

A codificação também merece atenção. Segundo a documentação, o File.WriteAllLines grava em UTF-8 sem BOM, e alguns programas exibem acentos de forma errada nesse caso.

// Grava em UTF-8 com BOM, o que ajuda o Excel a reconhecer os acentos
File.WriteAllLines("heroes.csv", lines, Encoding.UTF8);

A sobrecarga com Encoding.UTF8 inclui o identificador no início do arquivo. Assim, o Excel reconhece a codificação e exibe corretamente nomes com acentos.

Quando usar uma biblioteca

Para cenários simples, como relatórios internos e exportações pontuais, a abordagem deste artigo é suficiente. No entanto, arquivos com campos complexos, muitos tipos ou volumes grandes pedem uma biblioteca dedicada.

O CsvHelper é uma das opções mais conhecidas no ecossistema .NET. Ele trata aspas, cabeçalhos, culturas e mapeamento de propriedades, o que elimina boa parte do código manual.

Checklist para gerar CSV com segurança

Antes de colocar uma exportação em produção, vale revisar alguns pontos. A lista a seguir resume os cuidados que apareceram ao longo do artigo.

  • Defina a ordem das colunas e mantenha o cabeçalho sincronizado com ela.
  • Use um formato de data fixo, como yyyy-MM-dd, para não depender da cultura da máquina.
  • Escape vírgulas, aspas e quebras de linha conforme a RFC 4180.
  • Escolha o separador pensando em quem vai abrir o arquivo.
  • Grave em UTF-8 com BOM quando o destino for o Excel.
  • Prefira ReadLines e StreamWriter em arquivos grandes.

Também é uma boa ideia escrever testes para o operador implícito. Afinal, se a conversão de Hero para string mudar, toda rotina que precisa gerar CSV a partir dessa classe muda junto.

Por fim, lembre que um teste simples já protege bastante. Basta gerar CSV a partir de um objeto conhecido e comparar a linha resultante com o texto esperado.

Conclusão

Os arquivos CSV são uma mão na roda. Você os abre e manipula dentro do Excel, e eles são fáceis de trabalhar porque são apenas texto.

Neste artigo, combinamos implicit operators, extension methods e LINQ para gerar CSV e ler o arquivo de volta. Com isso, a conversão fica concentrada na classe Hero, e o código de gravação se resume a uma linha.

Precisamos apenas tomar cuidado com o leiaute do arquivo, o que fica fácil quando temos uma tipagem igual no C#. Para dados mais complexos, lembre de tratar aspas, separadores e codificação.

E, quando o volume crescer, considere gravar em streaming ou adotar uma biblioteca especializada.

FAQ

Preciso de alguma biblioteca para gerar CSV no C#?

Não, porque o CSV é apenas texto e o File.WriteAllLines já resolve a gravação. Bibliotecas como o CsvHelper ajudam quando os dados ficam mais complexos.

Por que o Excel abre o CSV com tudo em uma coluna?

Normalmente o Excel espera outro separador, como o ponto e vírgula em configurações brasileiras. Gere o arquivo com ; ou use o assistente de importação.

Como tratar valores que contêm vírgula?

Envolva o campo em aspas duplas e duplique as aspas internas, como define a RFC 4180. Na leitura, use um parser que entenda essas regras.

Qual a diferença entre ReadLines e ReadAllLines?

O ReadLines devolve as linhas sob demanda, enquanto o ReadAllLines carrega tudo em um array. Em arquivos grandes, o ReadLines costuma ser mais eficiente.

Por que os acentos aparecem errados no Excel?

O File.WriteAllLines grava em UTF-8 sem BOM por padrão. Use a sobrecarga com Encoding.UTF8 para incluir o identificador da codificação.