Como ler e escrever dados em arquivos CSV com C#

Neste artigo vou mostrar como é possível ler e escrever dados em arquivos CSV utilizando C# e uma biblioteca simples de utilizar.

No ano passado precisei implementar uma biblioteca que realizasse a migração de dados de um arquivo CSV para um banco de dados PostgreSQL, em um projeto em que trabalhei como consultor. Nessa época testei algumas bibliotecas, e acabei conhecendo o CsvHelper, que achei um dos mais simples para se implementar.


O pacote CsvHelper

O pacote utilizado neste artigo é o CsvHelper, que utiliza uma sintaxe bem simples para leitura e escrita de arquivos em formato CSV.

O primeiro passo para utilizá-lo é a instalação através do pacote NuGet disponibilizado.

Para instalar utilizando o Package Manager Console:

Install-Package CsvHelper

Para instalar utilizando a .NET CLI:

dotnet add package CsvHelper

Um ponto de atenção antes de iniciarmos o exemplo prático é que o CsvHelper exige que se especifique a CultureInfo a ser utilizado. Isso ocorre devido a convenções na interpretação de um CSV, que são afetadas por itens como caractere de separação, caractere de fim de linha e também no momento de se converter o tipo a partir do arquivo. Em geral o CultureInfo utilizado é InvariantCulture.

Exemplo prático

Vamos imaginar o seguinte cenário: você trabalha como programador em uma empresa que desenvolve soluções para clínicas médicas, e a ideia é oferecer um serviço de importação e exportação de dados relativos a consultas médicas na plataforma. O formato padrão definido é o CSV, e você é o responsável por implementar a funcionalidade. A primeira tarefa será criar uma prova de conceito para validar a biblioteca CsvHelper para verificar se ela vai atender o problema em mãos.

Sendo mais específico, será desenvolvida uma aplicação Console que vai:

  • Realizar upload de arquivo CSV
  • Realizar download de arquivo CSV

Por questão de simplicidade, o modelo de CSV a ser recebido é estático, e para o download não será passado nenhum filtro, todos os dados armazenados serão baixados no CSV. Além disso, eles ficarão armazenados em uma lista em memória, o que facilmente pode ser adaptado para um banco de dados através de uma abstração como o padrão Repositório (que utilizo para ilustrar melhor o exemplo). Se tiver dúvidas sobre o padrão Repository, indico clicar aqui e assistir este vídeo meu no YouTube.

Um arquivo de exemplo a ser lido e também criado é o seguinte:

id,nome_clinica,nome_paciente,data_nascimento,data_atendimento,especialidade
100,Eolanda,Clínica de Gastro 1,1981-04-20,2021-11-29,gastroenterologia
101,Sabina,Clínica de Gastro 2,1977-09-26,2021-11-30,gastroenterologia
102,Tilly,Clínica de Gastro 3,1949-04-27,2021-02-24,gastroenterologia
103,Kristina,Clínica de Dermatologia 1,1989-01-28,2021-03-23,dermatologia
104,Caressa,Clínica de Dermatologia 2,1966-07-07,2021-03-16,dermatologia
105,Britni,Clínica de Dermatologia 3,1999-06-14,2021-02-18,dermatologia
106,Ashlee,Clínica de Cardiologia 1,1987-01-22,2021-03-26,cardiologia
107,Max,Clínica Medica 1,1994-10-23,2021-08-22,clínica médica
108,Jaime,Clínica de Cardiologia 2,1961-03-24,2021-04-17,cardiologia
109,Katharina,Clínica de Dermatologia 2,1988-08-15,2021-01-16,dermatologia

Com a estrutura do arquivo definida, vamos para a criação da classe que será utilizada como modelo para importação e exportação de dados. Note a utilização do atributo Name do CsvHelper para mapear de uma coluna para uma propriedade da classe.

using System;
using CsvHelper.Configuration.Attributes;

namespace ArtigoCsv
{
    public class Atendimento
    {
        [Name("id")]
        public int Id { get; set; }
        [Name("nome_clinica")]
        public string NomeClinica { get; set; }
        [Name("nome_paciente")]
        public string NomePaciente { get; set; }
        [Name("data_nascimento")]
        public DateTime DataNascimento { get; set; }
        [Name("data_atendimento")]
        public DateTime DataAtendimento { get; set; }
        [Name("especialidade")]
        public string Especialidade { get; set; }
    }
}

É possível configurar esse mapeamento através de uma classe específica, que herda de ClassMap<T> onde T é a classe a ter suas propriedades mapeadas em operações envolvendo o arquivo CSV.

using CsvHelper.Configuration;

namespace ArtigoCsv
{
    public class AtendimentoMap : ClassMap<Atendimento>
    {
        public AtendimentoMap()
        {
            Map(m => m.Id).Name("id");
            Map(m => m.NomeClinica).Name("nome_clinica");
            Map(m => m.NomePaciente).Name("nome_paciente");
            Map(m => m.DataNascimento).Name("data_nascimento");
            Map(m => m.DataAtendimento).Name("data_atendimento");
            Map(m => m.Especialidade).Name("especialidade");
        }
    }
}

Eu tendo geralmente a preferir classes de mapeamento próprias do que entupir os modelos de atributos.

O código para leitura desse arquivo é bem simples:

using System;
using System.Globalization;
using System.IO;
using CsvHelper;
using CsvHelper.Configuration;

namespace ArtigoCsv
{
    class Program
    {
        static void Main(string[] args)
        {
            var config = new CsvConfiguration(CultureInfo.InvariantCulture)
            {
                HasHeaderRecord = true,
            };

            using (var reader = new StreamReader("/Users/luisfelipedeoliveiramesa/Documents/Projects/articles/ArtigoCsv/exemplo_artigo.csv"))
            using (var csv = new CsvReader(reader, config))
            {
                var atendimentos = csv.GetRecords();

                foreach (var atendimento in atendimentos)
                    Console.WriteLine($"Paciente: {atendimento.NomePaciente}, Clínica: {atendimento.NomeClinica}");
            }
        }
    }
}

Através da tipagem as operações em cima desse retorno fica muito fácil. Verifique na imagem abaixo a saída do programa.

Paciente: Clínica de Gastro 1, Clínica: Eolanda
Paciente: Clínica de Gastro 2, Clínica: Sabina
Paciente: Clínica de Gastro 3, Clínica: Tilly
Paciente: Clínica de Dermatologia 1, Clínica: Kristina
Paciente: Clínica de Dermatologia 2, Clínica: Caressa
Paciente: Clínica de Dermatologia 3, Clínica: Britni
Paciente: Clínica de Cardiologia 1, Clínica: Ashlee
Paciente: Clínica Medica 1, Clínica: Max
Paciente: Clínica de Cardiologia 2, Clínica: Jaime
Paciente: Clínica de Dermatologia 2, Clínica: Katharina

Simples, né? E sobre a escrita do arquivo, você pode ver o código abaixo. A sequência é:

  • Registro da classe de mapeamento
  • Escrita da linha das colunas do CSV
  • É passado para o próximo registro na escrita
  • São escritos os registros de dados efetivamente no CSV

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using CsvHelper;
using CsvHelper.Configuration;

namespace ArtigoCsv
{
    class Program
    {
        static void Main(string[] args)
        {
            var config = new CsvConfiguration(CultureInfo.InvariantCulture)
            {
                HasHeaderRecord = true,
            };

            List atendimentos;
            
            using (var reader = new StreamReader("exemplo_artigo.csv"))
            using (var csv = new CsvReader(reader, config))
            {
                csv.Context.RegisterClassMap();

                atendimentos = csv.GetRecords().ToList();

                foreach (var atendimento in atendimentos)
                    Console.WriteLine($"Paciente: {atendimento.NomePaciente}, Clínica: {atendimento.NomeClinica}");
            }

            using (var writer = new StreamWriter("testeFile.csv"))
            using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
            {
                csv.Context.RegisterClassMap();
                
                csv.WriteHeader();
                csv.NextRecord();

                csv.WriteRecords(atendimentos);
            }
        }
    }
}

Se não for especificado o ClassMap antes de se escrever o Header e os registros, os nomes das colunas utilizados serão o próprio nome das propriedades (a não ser que esteja utilizando atributos na classe ao invés da classe de mapeamento).


Quer alavancar sua carreira como Desenvolvedor(a) .NET?

Além de Desenvolvedor .NET Sênior, eu sou instrutor de mais de 700 alunos e também tenho dezenas de mentorados.

Conheça o Método .NET Direto ao Ponto, minha plataforma com mais de 800 videoaulas, com cursos cobrindo temas relacionados a linguagem C# e Programação Orientada a Objetos, APIs REST com ASP NET Core, Microsserviços com ASP NET Core, HTML, CSS e JavaScript, Desenvolvimento Front-end com Angular, Desenvolvimento Front-end com React, JavaScript Intermediário, TypeScript, Formação Arquitetura de Software, Microsoft Azure, Agile, SQL, e muito mais.

Inclui comunidade de centenas de alunos, suporte por ela, plataforma e e-mail, atualizações regulares e muito mais.

Clique aqui para ter mais informações e garantir sua vaga


Conclusão

Neste artigo foi apresentado como realizar a leitura e escrita de dados em formato CSV. Espero que tenha sido útil!

Se curtiu, compartilhe com amigos e colegas.