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<Atendimento>();

                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<Atendimento> atendimentos;
            
            using (var reader = new StreamReader("exemplo_artigo.csv"))
            using (var csv = new CsvReader(reader, config))
            {
                csv.Context.RegisterClassMap<AtendimentoMap>();

                atendimentos = csv.GetRecords<Atendimento>().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<AtendimentoMap>();
                
                csv.WriteHeader<Atendimento>();
                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).


Inscreva-se na lista de espera do Método .NET Direto ao Ponto, um treinamento completo sobre C#, APIs com ASP.NET Core, Microsserviços e mais:  Inscreva-se aqui.

São mais de 450 vídeo-aulas sobre temas como C#, ASP NET Core, EF Core, CQRS, Clean Architecture, Autenticação e autorização com JWT, Testes Unitários, HTML, CSS, JavaScript, Desenvolvimento Front-End com Angular além de mini-cursos em Microsserviços, Performance em .NET, ASP NET Core e Azure, Docker, Carreira Internacional em .NET, e mais. Cursos com certificado em plataforma moderna, e novas aulas são adicionadas semanalmente!


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.